Unit Tests: Part Two
8 min read15-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 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 }
1void sendSixtyDayExpirationNotice() {2 List clientsToEmail =3 fetchClientsWithExpiringRecords(60, 6, RecordStatus.Expiring)45 clientsToEmail.each { client ->6 content =7 """8 Some content personalized for the client9 Some list of records with links10 """1112 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 {}
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
- This will be imported from Spock and is located at
1class RecordServiceSpec extends Specification {}
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 RecordServiceSpec2 extends Specification implements ServiceUnitTest<RecordService> {}
Finally, we need to use the
DataTest
traitDataTest
provides access tomockDomains
- 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 RecordServiceSpec2 extends Specification3 implements ServiceUnitTest<RecordService>, DataTest {}
Recap:
DataTest
fromgrails.testing.gorm.DataTest
(Docs)ServiceUnitTest
fromgrails.testing.services.ServiceUnitTest
(Docs)Specification
fromspock.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 = notificationService3 }
Putting it all together
So our entire spec file now looks like this:
1class RecordServiceSpec extends Specification implements ServiceUnitTest<RecordService>, DataTest {23 @Shared NotificationService notificationService = Mock(NotificationService)45 def setupSpec() {6 service.notificationService = notificationService7 }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 record23setup() {4 record = new Record(5 // whatever info is required to create the Record6 )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 {23 @Shared NotificationService notificationService = Mock(NotificationService)45 Record record67 def setupSpec() {8 service.notificationService = notificationService9 }1011 def setup() {12 record = new Record(13 // whatever info is required to create the Record14 )1516 record.save()17 }18}
Some notes:
Using this method for creating a dummy
Record
, I can modify theexpirationDate
field from test to test without worrying about the changes persisting and breaking tests based on the order they run.I can only access the
notificationService
in mysetupSpec
because it uses the@Shared
tag. Otherwise, I'd need to spin it up before each test in thesetup
hook.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 thesetup
, not using@Shared
.
- I'm still researching it, but at certain points with Grails 3.3.6, I have needed to (unnecessarily) add the
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