How to write good tests
A secret to writing good tests is keeping in mind that there are only two reasons for a test to exist:
1. Making sure your code works as expected.
2. Avoiding regression bugs.
That’s it. There are no other reasons for a test to exist. Think of it whenever you face any of those questions:
- should I try to achieve 100% code coverage?
- should I write a test for this specific functionality or not?
- should I mock some specific dependency or not?
- what do I need to assert and what not?
Reason 1. Making sure your code works as expected
A good test helps to ensure that your code works as expected if it’s not obvious from your code. Do not write tests for something which is obvious from the code.
We all know how code may get complicated, may contain lots of non-trivial conditions or sequences of steps depending on one another. Sometimes you may have confidence that it works properly, but it may not be obvious for someone who reviews or updates your code.
It’s perfectly fine to write a test for such code.
But if this is not the case, if the code you try to test is trivial, then check if this test fits Reason 2.
Maybe your code does verify some trivial condition that is obvious from the code, but this condition is so universal and important that no possible refactoring or changing of functionality should ever break it. You absolutely do not want to have any regressions related to this condition. Then your test may qualify for Reason 2.
Reason 2. Avoiding regression bugs
A test capable of catching regression bugs is a test that rarely changes. Especially it should not change during refactoring of underlying code or adding new functionality. If it’s always rewritten or updated whenever you touch the underlying code, its regression-catching capabilities are zero.
Why? Because if you or someone else introduces a regression bug in the code during refactoring and then heavily rewrites or even removes the test at the same time, then this regression has a high chance of not being caught by this test.
A bad example is a test where all class dependencies are heavily mocked. The problem with overusing mocks is not the fact of mocking, but the fact that such test will be significantly rewritten whenever class, its dependencies, or their interactions are changed, which happens almost always during refactoring.
Maybe your test code is indeed coupled with the implementation, and you do have a reason to mock external dependencies, but the code under test is so complicated that there’s no way of looking at it and saying for sure it’s correct. A good example is an implementation of a complicated algorithm. Then your test may qualify for Reason 1.
The most useful tests are, of course, the ones that satisfy both Reason 1 and Reason 2.