Unit Tests: Part One
8 min read12-Feb-21
Today, I'm going to start deconstructing the process for actually testing that code! Full disclosure, it's maybe not as easy as I made it sound in the last post.
One big assumption I made in that article is the setup to add new unit tests is already done. Unfortunately, that's not the most fair assumption to make. The reality — especially in Grails-land — is that juggling Spock, Hibernate, GORM, and any dependencies your class might have can be quite tricky.
So, a refresher on the hypothetical service method I mentioned in my last post:
1Map fetchClientsWithExpiringRecords(2 Integer daysUntilExpiry,3 Integer dateRange,4 RecordStatus status5 ) {6 List<Record> allRecords =7 Record.findAllByExpirationDateBetweenAndStatus(8 new Date() + daysUntilExpiry,9 new Date() + daysUntilExpiry + dateRange,10 status11 )1213 // more code to parse `allRecords` for the relevant info14 // and grab the clients associated with the records1516 return clientList17 }
It takes the two elements to make a timespan (say, between 60 and 66 days from now) and a Record's status, and returns us all our clients with matching Records who need a notification.
And the un-tested method containing the bug:
1void sendSixtyDayExpirationNotice() {2 List clientsToEmail = fetchClientsWithExpiringRecords(60, 6, RecordStatus.Expiring)34 clientsToEmail.each { client ->5 content =6 """7 Some content personalized for the client8 Some list of records with links9 """1011 notificationService.email(client, content)12 }13}
Before we start writing tests, let's take a quick look at what we'll need our test file to do in order to work.
- A way to get valid responses from the
fetchClientsWithExpiringRecords
method- In our case, we use PostgreSQL with Hibernate as a layer in between our Grails app and the database
- We could mock the response, but for our purposes it's more important to have valid
Record
entries with specific expiration dates to check against
- A
Mock
of ournotificationService
and itsemail
method- I'll circle back to this in a second, don't worry
- A
Mock
of ourRecord
itself, with an expiration value we can manipulate- We'll want to write a few tests, each of which could change this value to confirm the desired boundaries in our method actually work
A few thoughts
In my next couple posts, I'll review the setup and discuss how I accomplished those three items. Then, I'll introduce an example of the unit test I wrote and walk step-by-step through its contents.
For now, I really want to present a few thoughts I have on some of these concepts.
What's in a name?
I'm calling these tests "unit tests," but understand they might cross the line to "integration tests" by some definitions. I don't care. These terms feel arbitrary, and seem like moving targets — at least in the context of a post like this.
Sure, when we break down Grails' @TestFor
mixin and look at the limitations of a true Grails Unit Test
vs an integration test it matters.
But here's what's important about our tests:
- They should mock as little as possible to isolate what we need to know
- They must test behavior, not the inner machinations of the method
- As a result of #2, changes inside the method that don't break I/O will not break our tests
Would a true "unit test" mock the response of the fetchClients
method instead of letting the tested method run its course? Maybe. Does mocking that accomplish what I actually want, and give me confidence in my code? Not really.
As a result, this is what I would consider to be the smallest "useful" test. As such, it's what I'd call a "unit test."
Note: I reserve the right to change my mind about this later, because I am not an expert.
Stubs, Mocks, Spies, Oh My!
My background working with .NET & Angular served me well in developing an understanding of SoC through MVC architecture. However, it has not helped at all in understanding Spock.
Before starting with GlobalVetLink and working with Grails, I would say 90% of the tests I wrote were in Jest. I was very comfortable with how Jest mocks could be leveraged to unit or integration test my code. Just toss a few jest.spyOn()
s in my Arrange block, maybe a couple mockFn.mockReturnValue()
or mockImplementation()
statements and call it a day!
It is a testament to the seriousness with which my prior employer approached testing, but I have never had to put real thought into the finer points of what I was actually doing. The tests just kind of... worked?
Spock isn't quite as simple.
Others might disagree, so take this all with a grain of salt, but I find the distinctions between Stub
and Mock
that Spock uses to be unintuitive. I frequently need to revisit the documentation to find my way, and frankly don't think the docs stack up to what Jest offers.
That said, it's critical to understand what each of these things means before starting to write unit tests with Spock.
Here is my working definition of each of the three methods of setting up a fake resource for a unit test:
Stub
A stub is a tool for dictating the response from a method provided by a dependency:
- Stubs do not provide any contextual details about the method or dependency
- When used, they don't provide insight to — importantly — whether the method was even called
- Used e.g.
injectedDependency.methodName(inputValue) >> valueRequired
- From Spock's docs: "Whenever the subscriber receives a message, make it respond with 'ok'."
Mock
Mocks get me into trouble all the time, because of the model I'm used to from Jest. Again, I am not an expert, but here is my general understanding of mocked methods in Spock:
- Mocks literally extend the class you are mocking
- They provide hooks into each of the methods defined in the extended class, which may then be overrided via Stubs.
- For my future self, here's a critical piece ripped right from the documentation:
1def "should send messages to all subscribers"() {2 when:3 publisher.send("hello")45 then:6 1 * subscriber.receive("hello")7 1 * subscriber2.receive("hello")8}
"Read out aloud: 'When the publisher sends a "hello" message, then both subscribers should receive that message exactly once.' When this feature method gets run, all invocations on mock objects that occur while executing the when block will be matched against the interactions described in the then: block."
Here's the key distinction from how I used Jest: the mocking (i.e. Jest's spyOn
) and the assertion happen in the same line.
That's a new paradigm to me, and trips me up regularly.
So to shift this method from a Mock to a Stub, it would be as simple as adding a response to the Mock >> "ok"
1def "should send messages to all subscribers"() {2 when:3 publisher.send("hello")45 then:6 1 * subscriber.receive(_) >> "ok"7}
Again, from the docs:
1subscriber.receive(_) >> "ok"2| | | |3| | | response generator4| | argument constraint5| method constraint6target constraint
Unlike Jest, where they are always declared separately, in Spock they must be declared in the same interaction.
In my head, mocking is part of the Arrange process. It's setting up data, classes, methods, etc. to do what they need to do so when you Act, you can isolate the interaction you're concerned with to reference in your Assertions. This is a different methodology, and it's important to be crystal clear with how everything interacts.
Spy
Okay, so if that covered Mocks and Stubs, what the heck is a Spy?
In the interest of transparency, I have never used a Spy in testing our Grails application. The need just hasn't arisen.
In fact, the Spock docs go so far as to say:
"Think twice before using this feature. It might be better to change the design of the code under specification."
This is a great indicator they are not referring to the same thing as Jest, where spyOn
is a pretty regular tool.
- Spies use the real underlying object, not a version spun up for the purpose of testing
- It can be used to replace return values on its methods with Stubs
- If a method is stubbed, the real method is no longer called at all — and no part of its inner workings are touched
- Like a Mock, it can be used to track the number of times a method is called
- Note: from what I have read, Spies may interact in odd ways with
@CompileStatic
, so readers will want to confirm that if they use much static typing
Wrap-up
Today, we looked at some code that needs unit tests. Then, we reviewed what the unit test file is going to need to include to make that possible. Lastly, we sat patiently while I rambled about some general thoughts regarding Grails tests.
Next time, I'll walk through the setup process to get the test file up and running. That will include each of the three items we identified as crucial to running the test.
This is part one of a three part series on unit testing a Grails app.
Previous
A Case For Unit Tests