Unit Tests: Part Two

8 min read
15-Feb-21

Today I'm picking up right where I left off in my first post in this miniseries on unit testing a Grails app.

We're looking at writing some simple unit tests to cover common input-output (I/O) for a service method. Before we can do that, we need to prep the test file.

Laying the ground work

Here are the two methods I referenced last time. We'll primarily focus on testing the second method, but our setup needs to account for both.

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 }
1void sendSixtyDayExpirationNotice() {
2 List clientsToEmail =
3 fetchClientsWithExpiringRecords(60, 6, RecordStatus.Expiring)
4
5 clientsToEmail.each { client ->
6 content =
7 """
8 Some content personalized for the client
9 Some list of records with links
10 """
11
12 notificationService.email(client, content)
13 }
14}

Hooking into Hibernate

First up is mocking valid responses from our fetchClients method. To do that, we'll need some means to interact with Hibernate, which we use as an interface between Grails and PostgreSQL.

Here are the extensions and traits we'll use, with an explanation for each, to set this up. Each step will make a modification to our class definition, which starts as class RecordServiceSpec {}

  1. Our spec class will extend Specification

    • This will be imported from Spock and is located at spock.lang.Specification
    • It ensures our tests are run with Spock's own JUnit runner, Sputnik
1class RecordServiceSpec extends Specification {}
  1. We will need to implement Grails' ServiceUnitTest trait

    • This is required for the class to be registered with the application context
    • It accepts a parameter for the service class to mock (serviceClass)
1class RecordServiceSpec
2 extends Specification implements ServiceUnitTest<RecordService> {}
  1. Finally, we need to use the DataTest trait

    • DataTest provides access to mockDomains
    • These domain mocks provide standard GORM behavior, without requiring a database
    • Instead of hooking directly into a Postgres instance, they will spin up a concurrant hash map in memory

So our class definition with all these tools in place looks something like this:

1class RecordServiceSpec
2 extends Specification
3 implements ServiceUnitTest<RecordService>, DataTest {}

Recap:

  • DataTest from grails.testing.gorm.DataTest (Docs)
  • ServiceUnitTest from grails.testing.services.ServiceUnitTest (Docs)
  • Specification from spock.lang.Specification (Docs)
  • And RecordService from wherever we put our service (presumably "Services" within our parent package dir)

Mocking the Dependency

One outcome of our method successfully firing is an email being sent to clients. As a result, we inject a dependency to the RecordService called the NotificationService to access its public email() method.

In this case, I'm less concerned with the specifics of the email method. I really want to know it's called once for each unique client, and I really want to know it's called with the right parameters. But beyond that? I can rely on the unit and integration tests in place for my NotificationService to ensure once dispatched the method behaves appropriately.

So using the operating definitions I laid out last time, we're going to want a Mock of the NotificationService, rather than a Stub. This will give us insight into the number of times the method is called and the parameters with which it's called.

What does this look like?

First, we'll declare and mock the service:

1@Shared NotificationService notificationService = Mock(NotificationService)

The @Shared decorator here provides access to the exact same object in memory across all our tests, similar to creating our Mock in the setupSpec lifecycle hook. It's important to use this judiciously, as it can be an opportunity to make your tests non-deterministic. Because we're only concerned with spinning up a service to count the number of times its method is called and won't be mutating any data it's a slight convenience to use this decorator.

More info on @Shared can be found in the Spock docs here.

Now that we've mocked the dependency, we can add a new setupSpec block for our spec file. This works similarily to Jest's beforeAll (just like setup would run at the same time beforeEach does). More info on the lifecycle hooks available from Spock's Specification can be found here.

The setupSpec hook will handle injecting the mocked service into our RecordService through simple assignment:

1def setupSpec() {
2 service.notificationService = notificationService
3 }

Putting it all together

So our entire spec file now looks like this:

1class RecordServiceSpec extends Specification implements ServiceUnitTest<RecordService>, DataTest {
2
3 @Shared NotificationService notificationService = Mock(NotificationService)
4
5 def setupSpec() {
6 service.notificationService = notificationService
7 }
8}

Mocking the Record

This is the last piece of the puzzle before we can move forward with writing our tests.

I promise the time invested in proper setup will pay off, though, once we get to a point where tests take just a few seconds to add.

A Note on Behavior

In my last post, I pointed out we should mock as little as possible to isolate what we are testing. This desire has to be balanced against making our test non-deterministic. I alluded to this earlier and want to expand on it a little bit.

Consider a situation in which we write assertions relating to a string literal timestamp. This could fail every time there's a new day so it's critical the data we use in our mock are impervious to timezones or date changes.

We want to ensure we carefully dispose of any data mutations that could persist beyond the scope of one individual test. For the record, this is why we don't always want to query a live database for testing. The order in which tests or test suites run should have no bearing on the pass/fails we get in the console.

For a reference on non-deterministic tests (and the danger they pose), here is a piece I liked from Martin Fowler: Eradicating Non-Determinism in Tests.

Building the Record

In order to build our Record, we'll need to use one of the internal methods exposed by the DataTest trait. Instructing Sputnik to mock the right domain class is as simple as using this declaration at the top of our file:

1Class<?>[] getDomainClassesToMock() {
2 return [Record] as Class[]
3}

This is a handy helper, and works in a fairly intuitive way. If we had other domains to mock, we could add those to the array here and just let DataTest do its thing.

Technically, this method is provided by the DomainUnitTest trait, so you could avoid reaching for DataTest if you found it too heavy. For a reference on this trait, see this doc.

Now that we have a mock for the underlying class, creating a new Record is as easy as instantiating it. Notice here I do not use @Shared, because I would not like reuse the same object between tests. Instead, I will be using the setup hook to instantiate the Record - remember, @Shared is the same as setupSpec!

1Record record
2
3setup() {
4 record = new Record(
5 // whatever info is required to create the Record
6 )
7}

The last thing we need to do is save the Record so the DataTest trait will manage it like a saved value in Postgres. This is as simple as adding a record.save() to the setup() hook.

Altogether, here's what our file looks like:

1class RecordServiceSpec extends Specification implements ServiceUnitTest<RecordService>, DataTest {
2
3 @Shared NotificationService notificationService = Mock(NotificationService)
4
5 Record record
6
7 def setupSpec() {
8 service.notificationService = notificationService
9 }
10
11 def setup() {
12 record = new Record(
13 // whatever info is required to create the Record
14 )
15
16 record.save()
17 }
18}

Some notes:

  1. Using this method for creating a dummy Record, I can modify the expirationDate field from test to test without worrying about the changes persisting and breaking tests based on the order they run.

  2. I can only access the notificationService in my setupSpec because it uses the @Shared tag. Otherwise, I'd need to spin it up before each test in the setup hook.

  3. I have encountered situations in which I can't use the code above.

    • I'm still researching it, but at certain points with Grails 3.3.6, I have needed to (unnecessarily) add the GrailsUnitTest trait
    • I have also had to instantiate the notificationService in the setup, not using @Shared.

In my next post, I'm going to wrap up the unit tests by actually, you know, writing some unit tests!


This is part two of a three part series on unit testing a Grails app.

Previous
Unit Tests: Part One

Next
Unit Tests: Part Three