I saw a pull request: forty new tests, all serving a single purpose — pushing coverage to 100%. Most of them called a getter and verified that the value just set came back. Coverage turned green. The number of bugs those forty tests would catch: zero.

Test coverage is a useful number — until you turn it into a target.

What coverage measures, and what it doesn’t

Coverage measures one thing: which lines of code run while the tests execute. What it doesn’t measure matters far more: whether those lines are actually verified.

A line counts as “covered” even by a test with no meaningful assert. The code runs, the test is green, but nobody has checked whether the code behaves correctly. Coverage can’t see the difference between code that runs and code that’s verified.

The wrong metric rewards the wrong test

Once coverage becomes a target, human behavior is predictable: people write the tests that raise the number by the cheapest route.

  • Getter/setter tests — testing code that has no logic.
  • Tests that verify a mock was called — an interaction, not a real result.
  • Tests whose assert is meaningless, which only say “it didn’t blow up”.

These raise the coverage number and catch no bugs. Worse: a green 100% gives the team false confidence. Code that’s assumed to be tested has merely been executed.

The measure of a good test

A test’s value lies not in the line it covers but in the bug it catches. A simple check: deliberately break the logic of the code — turn a + into a -, invert a condition. A good test goes red. If it doesn’t, that test isn’t verifying anything.

The systematic form of this idea is mutation testing: the tool breaks the code in small ways and measures whether your tests catch it. Coverage says “did the line run”; mutation testing says “was the line actually tested”.

Coverage is a byproduct

The right order is the reverse: test the behavior that matters first, and let coverage rise as a byproduct. Chase the number and you write tests that do nothing; chase the behavior and coverage comes anyway — and the coverage you get is real.

100% is not a target. A critical path that’s never tested can exist even in a 100% codebase; conversely, a system with 75% coverage may be solidly testing every risky path.

Where to spend the testing budget

Writing tests is also writing code; it has a limited budget. Spend that budget according to risk:

  • Complex logic, code that handles money, calculations.
  • Edge cases — boundary values, empty input, concurrency.
  • Places that have broken before — if a bug came out of there once, it will again.

Testing trivial, logicless code wastes the budget. The “the test setup is longer than the test itself” sign I mentioned in signals that a system has grown too complex connects here too: if a test is very hard to write, the problem isn’t the test, it’s that the code under test is too tightly coupled.


A coverage percentage is not a source of confidence; it’s only an observation. Confidence comes from the bugs your tests catch — not the number they chase.

Aim not for the number, but for the test that goes red when the code is wrong.