Unit Tests: Part One

8 min read
12-Feb-21
Last time, I wrote about a bug I recently introduced on prod that could have been caught with some basic unit tests.

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 status
5 ) {
6 List<Record> allRecords =
7 Record.findAllByExpirationDateBetweenAndStatus(
8 new Date() + daysUntilExpiry,
9 new Date() + daysUntilExpiry + dateRange,
10 status
11 )
12
13 // more code to parse `allRecords` for the relevant info
14 // and grab the clients associated with the records
15
16 return clientList
17 }

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)
3
4 clientsToEmail.each { client ->
5 content =
6 """
7 Some content personalized for the client
8 Some list of records with links
9 """
10
11 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.

  1. 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
  2. A Mock of our notificationService and its email method
    • I'll circle back to this in a second, don't worry
  3. A Mock of our Record 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:

  1. They should mock as little as possible to isolate what we need to know
  2. They must test behavior, not the inner machinations of the method
  3. 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")
4
5 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")
4
5 then:
6 1 * subscriber.receive(_) >> "ok"
7}

Again, from the docs:

1subscriber.receive(_) >> "ok"
2| | | |
3| | | response generator
4| | argument constraint
5| method constraint
6target 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

Next
Unit Tests: Part Two