When testing code there are four ways to handle the dependencies that code has:
- Plug in the real thing
- Use a mocking framework
- Use an in-memory fake of the service
- Use a dockerised alternative to the real thing
When writing end to end integration tests, on the whole we plug in the real services, and maybe only mock the very extreme dependencies on the outside of the system.
When writing service tests – a test that tries out a whole microservice against its dependencies – we have a hybrid situation to consider.
With unit tests, it’s real code and mocks all the way.
Let’s look at the criteria.
Plug In The Real Thing
If it’s pure stateless code, just use it. Try not to mock algorithms in tests. Similarly, never mock a data object. Just pass one in.
If it’s a heavy service like a database or cloud provided SaaS, then plugging in the real thing can be slow, or allow state to bleed between tests.
Somewhere in the middle is where we want to use something fake.
Martin Fowler talks about sociable and solitary test fixtures. A sociable fixture uses the unit under test with its real collaborators. A solitary one isolates it.
Bear in mind that solitary testing is an easy road to testing the implementation, not the real-world behaviour.
A mocking framework allows us to quickly replace an object with a fake, or easily identify how a fake object was used.
Sometimes it’s easier to plug in an alternative object. For example, we can pass in a locally created object that has the right interface, or an ad-hoc function. However, the more complex the behaviour we want this to have, the more a mocking framework can help us intercept the right calls, or easily compose the right fake behaviour.
However, mocking frameworks get harder and harder to control for stateful resources, like database layers or queues. There comes a point where a full blown fake may be a better option.
For a fake to be successful:
- Try to simplify the production code’s dependency on the real-world service, making it easier to fake
- Create some sort of in-memory fake implementation which is as good as the real service, but implemented simply
- Add unit tests for your fake, if it’s going to be used a lot
- Consider how to make it thread safe if using it with multi-threaded code
However, don’t use fakes if mocks or dockerised services are a better choice. A fake is a big overhead; it’s worthwhile if it can remove a lot of awkward stateful mocking, especially where it feels like the mock is testing the implementation and that the mocking tests are not stable to small changes in API use that aren’t materially changing behaviour, but do upset the mocking.
Similarly, don’t re-implement something where a known docker container can just provide the thing you want to mock.
Forgive me, because sometimes my tests run very slowly.
If it’s significant to test the code against the real database or service, because part of the implementation is the actual client to the outside world, or the whole application startup, then bringing some dependent services together in Docker and spinning up the application to point to them is a good move.
It’s much slower than unit tests, and is ideally used in service-sized integration testing, or perhaps to test specific code that’s used as a client to the external service.
Bizarrely, making the application compatible with certain Docker-based alternatives, may require some extra work. However, if you can, on your development machine, get the chance to debug the application running against most of its dependencies in some shape or form, then you have a fast feedback loop for deployment…
… but at the cost of your build times being slower than ideal…
… and maybe your build environment needs to support Docker a bit harder than average.
There’s no one size fits all for testing. However, using each of the above techniques in its rightful place will do wonders, where using it in the wrong place will be counter productive or lead to misleading feedback from the tests.