OBGM-302 Add unit tests using Spock#4582
OBGM-302 Add unit tests using Spock#4582ewaterman merged 3 commits intofeature/upgrade-to-grails-3.3.10from
Conversation
jmiranda
left a comment
There was a problem hiding this comment.
My initial comment is around PR conventions rather than the content of the PR. I know we have all of these documented somewhere but I struggling to find them. So for now, I'll add them here and then try to find the right place to reference them or (if they are missing) add them.
- Review point (11) of the Onboarding Guide (first link in Resources below) for the general flow
- When creating a branch, use a name that starts with the ticket number
- The name can be descriptive but should always include a Jira ticket number or GitHub issue number
- If a ticket doesn't exist, create one
- If a ticket exists, the branch name should be OBPIH-#### and optionally OBPIH-####-abbreviated-ticket-summary
- OBGM-319 or OBGM-319-unit-test-poc
- OBPIH-4288 or OBPIH-4288-spock-unit-tests
- 100 or 100-improve-performance-qoh-calculation
- OBS-1743 or OBS-1743-mass-product-deactivation
- NOTE: You will almost never create a branch for an OBS ticket unless you are working on the openboxes-devops repository
- The PR title should include the ticket number and summary
- When creating a PR, GitHub will usually pick up the name from the commit message (if there's just one) or the branch name (if there's more than one)
- Commit messages should include ticket number and a summary of the scope of work (or ticket summary)
- If you plan on multiple commits you can alternatively add a specific description for the scope of work in that commit to differentiate between commits
- If you have a commit or PR that addresses two or more tickets, include the ticket numbers at the end in parentheses
- Working on automation of unit tests (OBGM-319, OBPIH-4288)
Resources
- https://pihemr.atlassian.net/wiki/spaces/OB/pages/1406697500/Onboarding+Guide+-+Developers
- https://pihemr.atlassian.net/wiki/spaces/OB/pages/2782035970/Backend+coding+conventions
- https://pihemr.atlassian.net/wiki/spaces/OB/pages/2781970433/Code+Review+checklist+-+WIP
- https://github.com/openboxes/openboxes/blob/develop/CONTRIBUTING.md
- https://github.com/openboxes/openboxes/wiki/Contributing-a-Pull-Request
|
/me added the rest of the team as reviewers. |
|
@ewaterman Nice work on the testing documentation. I'll review and add my comments when I get a chance. |
kchelstowski
left a comment
There was a problem hiding this comment.
Most of my comments are regarding the formatting or questions whether I understand the code correctly.
| class ProductApiControllerSpec extends Specification implements DataTest, ControllerUnitTest<ProductApiController> { | ||
|
|
||
| @Shared private def forecastingServiceStub | ||
| @Shared private def productServiceStub |
There was a problem hiding this comment.
we used to add annotation above a variable, in this case:
@Shared
private def forecastingServiceStubThere was a problem hiding this comment.
yeah I usually only keep them on one line for things like tests, which potentially need to stub/declare a bunch of simple fields. This way they only get one line each and so don't clog up the file too much but I'm happy to split them up so that we stay consistent.
| mockDomains(Product, Location) | ||
| } | ||
|
|
||
| void setup() { |
There was a problem hiding this comment.
there seems to be too much indentation (between setupSpec and the setup methods)
There was a problem hiding this comment.
yes I think this is because I mixed tabs and spaces. From what I can see our code standard is 4 spaces so I'll reform the file. I used tabs for years so it's going to take a minute to shake that convention out of me hehe.
(As a side note, we should really have a standardized prefs file that we can import to Intellij so that all our setups stay in sync.)
| forecastingServiceStub = Stub(ForecastingService) | ||
|
|
||
| controller.forecastingService = forecastingServiceStub | ||
| controller.productService = productServiceStub |
There was a problem hiding this comment.
the indentation seems to be too big - we usually use 2 for tab.
There was a problem hiding this comment.
I think for groovy files we have 4 (at least that's what I have tab size: 4, indent: 4, Continuation indent 8), 2 for tab we have for js
| then: | ||
| JSONObject json = getJsonObjectResponse(controller.response) | ||
| if (shouldFindLocation) json.data.location != null | ||
| else json.data.location == null |
There was a problem hiding this comment.
as for if/else statements, we used to write them with the { }, in this case it'd be:
if (shouldFindLocation) {
json.data.location != null
} else {
json.data.location == null
}btw I have a question what the json.data.location != null does, as it would return a boolean (it compares the json.data.location with the null)?
There was a problem hiding this comment.
yes that makes sense. I'll fix.
Actually you're right. Spock treats anything in the "then:" block that returns a boolean the same as if you prefixed it with "assert". If the condition returns false, it'd fail the test. BUT if the statement is nested in an if or else, then you need to explicitly provide the "assert" for it to trigger. I'll fix
| if (shouldFindProduct) json.data.product != null | ||
| else json.data.product == null | ||
|
|
||
| json.data.demand == [:] |
There was a problem hiding this comment.
wasn't it supposed to initialize the json.data.demand with an empty Map? This line just checks if json.data.demand is equal to the empty map.
There was a problem hiding this comment.
It's supposed to be an == because I'm testing that it returned the data from the mocking that I did in forecastingServiceStub.getDemand(_, _, _) >> [:]. I suppose I could mock something more interesting there though so it's more obvious that it's actually returning something.
There was a problem hiding this comment.
@ewaterman I haven't written many tests, so I didn't notice it is a shortcut of the assert. If it is the case, then I'm fine with that.
| @Shared private def forecastingServiceStub | ||
| @Shared private def productServiceStub |
There was a problem hiding this comment.
nitpicky:
| @Shared private def forecastingServiceStub | |
| @Shared private def productServiceStub | |
| @Shared | |
| private def forecastingServiceStub | |
| @Shared | |
| private def productServiceStub |
+ Please, use types always where it is possible
| void setupSpec() { | ||
| mockDomains(Product, Location) | ||
| } | ||
|
|
||
| void setup() { | ||
| productServiceStub = Stub(ProductService) | ||
| forecastingServiceStub = Stub(ForecastingService) | ||
|
|
||
| controller.forecastingService = forecastingServiceStub | ||
| controller.productService = productServiceStub | ||
| } |
There was a problem hiding this comment.
The indents here look really odd, I am not sure if this is because of the GitHub or if it is broken in the code
There was a problem hiding this comment.
same as above. I'll reformat
| if (shouldFindLocation) json.data.location != null | ||
| else json.data.location == null |
There was a problem hiding this comment.
We are using braces in if body
| if (shouldFindLocation) json.data.location != null | |
| else json.data.location == null | |
| if (shouldFindLocation) { | |
| json.data.location != null | |
| } else { | |
| json.data.location == null | |
| } |
| mockDomain(Person, [person2, person4, person3, person1]) | ||
|
|
||
| expect: | ||
| Person.list().sort() == [person1, person2, person3, person4] |
There was a problem hiding this comment.
I am not sure if this is correctly compared. I mean if we are comparing the content of the array or just the references between those two objects 🤔
There was a problem hiding this comment.
This test is just verifying that sorting works so as long as the correct Person objects are in the correct places in the list we consider this a pass. Other tests verify the actual fields themselves. Changing the order in the expected array to something like [person1, person3, person2, person4] correctly fails the test so I feel confident this is working.
|
|
||
| void 'Person.toJson() should return as expected'(){ | ||
| given: | ||
| def person = new Person(id: id, firstName: firstName, lastName: lastName, email: email).save(validate: false) |
| requisition.validate() | ||
| assertNull requisition.errors["dateRequested"] | ||
| given: | ||
| def requisition = new Requisition(dateRequested:new Date()) |
There was a problem hiding this comment.
| def requisition = new Requisition(dateRequested:new Date()) | |
| Requisition requisition = new Requisition(dateRequested: new Date()) |
There was a problem hiding this comment.
I agree. I didn't change this one because I wanted to show the minimum number of changes needed to refactor the test but since I'm changing that line anyways, yes I'll update it.
There was a problem hiding this comment.
actually, these defs are all over the file and so it'd feel weird to just refactor this one case and not go through and refactor all of the tests, many of which are unchanged, so I'm just going to leave it as is for now.
There was a problem hiding this comment.
I am fine with that 😄
This PR contains the following:
These changes are based on the Unit Testing Standards document I created here: https://pihemr.atlassian.net/wiki/spaces/OB/pages/3013869579/Backend+Unit+Testing+Standards
I'm 100% open to changes to those guidelines at this point and would love to have a general discussion of what people think our best practices should be going forward.
Hopefully these sample tests an give us a starting point for writing more unit tests and getting our code coverage to reasonable levels.