Throughout the Test Smells catalogue, there are signs you may be testing your code, rather than its behaviour. Even in the humblest of unit tests, the overriding rule should be:
Make your code perform its job and see what the result is.
Developers who write code and its tests can often misunderstand this as produce tests for each line and branch of the code. The irony is that we often measure the success of unit testing with this exact metric.
The Aim Is Not Line Coverage
The aim of a test is to exercise all the scenarios that the code is meant to accommodate. We should construct the situation in which the code is meant to do a job, exercise the code, and look for the important signs that the outcome is as expected.
That means we are not looking to see that the right lines of code were executed, but that the output of whatever code has executed is correct.
It Is Easier When You Start With Failing Tests
One reason it’s easier is that you don’t get distracted by the code you have written and start testing the code. Another is that you get to focus on the question that your code is meant to answer and the way that its answer might be detected. This, in turn, can prevent design flaws like chasing the dragon.
My Test Is Good Isn’t It?
How might you check a test for signs of it testing the implementation, rather than the behaviour?
- Does it have mocks in? If so, what are you verifying on the mocks?
- If you subtly change the exact implementation of the algorithm with the same outcome, does the test break?
- Do you have extra assertions after the one on the result, checking that the right mocks were called?
- Are you stubbing second, third, or even fourth-degree calls on mocks?
- Do your test scenarios talk about the code in their names?
- Are you testing the internal monologue of the code, or its outcome?
A lot of the implementation tests I’ve seen make heavy use of mocking. It’s not that mocks are wrong… it’s just that they enable this sort of test so much, it’s hard to spot when you cross the line into testing implementation.
Fakery Is More Genuine
A test double, rather than a mock, which provides an in-memory implementation of the service you wish to call, can be a much better thing to use to bring tests back to behavioural tests. Even dockerising some services can make your testing more genuine and more realistic than mocking things.
On the whole, there’s a balance to strike. Using test frameworks that provide in-memory stubs for things is a great way forward and decouples the test from the implementation. Defining wrappers on services and producing your own tested test doubles for them is also a decent option.
- Write tests first
- Don’t write a mock verify call if you don’t need to
- Consider fakes/test doubles in place of mocks
- Focus on provable outcomes, not functions called