Skip to content

OBGM-302 Add unit tests using Spock#4582

Merged
ewaterman merged 3 commits intofeature/upgrade-to-grails-3.3.10from
feature/template-unittests
Apr 16, 2024
Merged

OBGM-302 Add unit tests using Spock#4582
ewaterman merged 3 commits intofeature/upgrade-to-grails-3.3.10from
feature/template-unittests

Conversation

@ewaterman
Copy link
Member

This PR contains the following:

  • An example refactor of a JUnit test (RequisitionTests)
  • An example refactor of the old @TestMixin approach (CategoryApiControllerTests)
  • An example implementation of a domain unit test (PersonSpec)
  • An example implementation of a service unit test (ProductServiceSpec)
  • An example implementation of a controller unit test (ProductApiControllerSpec)

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.

@ewaterman ewaterman self-assigned this Apr 12, 2024
Copy link
Member

@jmiranda jmiranda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@jmiranda
Copy link
Member

/me added the rest of the team as reviewers.

@jmiranda jmiranda added the domain: documentation Changes or discussions relating to documentation label Apr 12, 2024
@jmiranda
Copy link
Member

@ewaterman Nice work on the testing documentation. I'll review and add my comments when I get a chance.

Copy link
Collaborator

@kchelstowski kchelstowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we used to add annotation above a variable, in this case:

@Shared
private def forecastingServiceStub

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there seems to be too much indentation (between setupSpec and the setup methods)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the indentation seems to be too big - we usually use 2 for tab.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 == [:]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines 25 to 26
@Shared private def forecastingServiceStub
@Shared private def productServiceStub
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicky:

Suggested change
@Shared private def forecastingServiceStub
@Shared private def productServiceStub
@Shared
private def forecastingServiceStub
@Shared
private def productServiceStub

+ Please, use types always where it is possible

Comment on lines 28 to 38
void setupSpec() {
mockDomains(Product, Location)
}

void setup() {
productServiceStub = Stub(ProductService)
forecastingServiceStub = Stub(ForecastingService)

controller.forecastingService = forecastingServiceStub
controller.productService = productServiceStub
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above. I'll reformat

Comment on lines 60 to 61
if (shouldFindLocation) json.data.location != null
else json.data.location == null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using braces in if body

Suggested change
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]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

types

requisition.validate()
assertNull requisition.errors["dateRequested"]
given:
def requisition = new Requisition(dateRequested:new Date())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def requisition = new Requisition(dateRequested:new Date())
Requisition requisition = new Requisition(dateRequested: new Date())

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with that 😄

@ewaterman ewaterman changed the title sample unit tests for controllers services and domains OBGM-302 Add unit tests using Spock Apr 15, 2024
@ewaterman ewaterman merged commit bcc4ecb into feature/upgrade-to-grails-3.3.10 Apr 16, 2024
@ewaterman ewaterman deleted the feature/template-unittests branch April 16, 2024 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: documentation Changes or discussions relating to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants