π Reading time: 3 minutes
Watching the code coverage percentage go up lets us sleep soundly. But is our system really tested, or only seemingly so?
π Branch coverage
Let's look at the screenshot below, in particular the line where the isNotStringOrNotBlankString Predicate is invoked. We have true hits: 161, false hits: 0.
What does that zero tell us? That, despite the 333 total passes, we never went through the branch where that predicate fails. This is usually a symptom of one of the following two things:
- incomplete tests: we forgot to test an input case (e.g. an empty string or a particular object)
- dead code: that condition is logically impossible to reach because of earlier checks. If a branch is never taken, why keep it?
Branch coverage is the best "pruner" of useless code you can have!
π The combinatorial trap
Let's get to the second point: testing only through integration tests is an attempt to solve a combinatorics problem with brute force.
Let's imagine a system with L layers and M methods, where each method has logical branches. If we try to test everything through integration, the combinations of inputs that have to traverse the various layers grow exponentially. Testing every possible scenario by trying every single path, always starting from the front door, is computationally unrealistic.
π Unit tests are the "cheat code"
- from exponential to linear complexity: by testing the single method (like the one in the screenshot), we drastically reduce the number of tests needed. If every piece is verified in each of its branches, their combination is mathematically more solid
- extreme lightness: while the integration test is still "spinning up" the Spring context or the Docker containers, the unit tests have already verified a gazillion branches
- surgical precision: if this test fails, we know exactly that the problem is in the predicate on line 56. If the integration fails... well, happy treasure hunt across the various layers
Hey, don't get me wrong, I'm not saying integration tests are useless: they're the ones that tell us the system really runs under realistic conditions. But it's the unit tests that validate the logic and tell us whether each little piece actually works. Ignoring branch coverage and relying only on end-to-end flows means looking at the system from afar, where everything apparently works, but taking for granted what happens inside.
See you at the next pill! β
