Sometimes you may need to test that an asynchronous event has been triggered in your code but you can end up compromising on the quality of your tests when doing so.
This post will show one technique to test this behaviour whilst keeping your tests clean and not complicating your production code
It's assumed you're familiar with writing tests, interfaces, go routines, channels and
select from Go.
The code to test
We are writing a service which has a dependency of a repo passed to it. Plain and simple DI for separation of concerns.
The service has a method
FaveAndSave which takes a
string and it will do some amazing business logic and then save to the repo.
The code calls the
repo in a new go routine which makes testing a little tricky.
Let's test it
The benefit of using DI here is that we can now stub our
Repo, make it behave how we like and see how it's called.
This test fails wrongly because the save event is fired off in a separate go routine which ends up being executed after our assertion.
This means the
saveCalled value is still set to
false when we make our assertions.
Solution 1 - Sleepy time
We can add a sleep into our test to let the go routine finish before making assertions. But there are a few problems:
- The sleep length I put in is fairly arbitary and it's hard to know precisely what value it should be. You dont want to set it to be too long because then your test will be slow, but too short will make the test flaky.
- It's noise in our test. In this simple example it doesn't seem so bad but it is ultimately more cruft which we need to try and avoid.
- Sleeps always feel yucky!
Solution 2 - Use a channel in our stubs
We can embrace the asynchronousness of our production code so we dont have to rely on guessing how long the stub takes to get called.
Rather than setting a normal value we have a channel which gets written to when the stub is called. This means our test can wait on a value to appear on the channel, rather than using sleeps and hoping the value gets set.
This too has problems:
- If the stub isn't called like we expect then the test will fail, but because of deadlock/test time out. This means our test output is less helpful then it should be.
- The test now has channels and general asynchrous behaviour which is implementation detail that we dont really need to care about, like the sleeps it is noise.
Solution 3 - Make the stub's value "blocking" with a timeout
It seems these days many developers automatically recoil in disgust when you say "blocking" but it can often be simpler than the alternative.
return and generally a lot of ansynchonous cruft because everything is asynchronous.
That can make writing tests quite taxing, especially if you are new to it. Common gotchas include tests not actually running assertions or tests running forever.
Let's keep our tests clean by putting just a little bit of code in to our stubs to make them block for the event we care about
We have moved the responsibility of "Has the stub been called in a timely manner?" to the stub rather than the test by simply adding the method
The code takes advantage of Go's
select syntax to block until the value is written or until the timeout occurs.
This feels like a good separation of concerns. Now if we look at the actual test we can see it reads very clearly and has no cruft around go routines or channels.
The code in the stub maybe looks a little involved but you will definitely move these into separate files, perhaps even
go generate can be used to auto generate them like goautomock.
This post was motivated by some work I am doing where we are writing
a "listener" to a RabbitMQ channel. The listener takes a
chan Message, gets
fired off in a go routine and processes messages as they come in to the system.
We went through similar iterations described above until we arrived at solution 3.
Is this idiomatic Go? I'm not sure I know what idiomatic Go is yet but this solution to me ticks a number of boxes I care about in writing software irrespective of the programming language.
- Easy to read and write test code
- Separation of concerns in regards to "has this function been called?"
- Good test output on failure
Hopefully this has been interesting. Know a better way? Flame me on twitter @quii
For the sake of terseness this example just sees if
Repo is called, but you can of course see what it was called with. In the real world
Repo would probably have return values that we'd stub for tests too.
I like to pass
t.Log to my stubs so they can log diagnostic info (such as "I timed out waiting for
x"), it has the added bonus they only appear if the test fails