Why I've stopped enforcing unit test coverage thresholds
After years of pushing for 80% coverage, I've changed my mind. Here's the reasoning that got me there.
A change of heart
For the better part of a decade, I treated unit test coverage as a non-negotiable quality gate. Every PR had to meet the 80% threshold. Every service had a SonarQube badge proudly displaying its coverage number.
I was wrong. Or at least, I was optimizing for the wrong thing.
What actually happens
When you enforce arbitrary coverage thresholds, three things consistently happen:
-
Tests become a checkbox exercise. Developers write tests that exercise code paths without meaningful assertions.
assertTrue(true)is not a joke — I’ve seen variations of it in production codebases. -
Implementation details get tested instead of behavior. Every private method gets its own unit test. Refactoring becomes painful because you’re not just changing code — you’re updating a dozen tests that coupled themselves to the old structure.
-
The expensive bugs still slip through. Unit tests don’t catch integration failures, race conditions, or deployment misconfigurations. The outages I’ve been paged for at 2 AM were never caused by a missing unit test.
What I advocate now
- Write unit tests for pure business logic — the stuff where you can clearly define inputs and expected outputs.
- Push testing effort toward integration and end-to-end tests. Contract tests between services have caught more regressions than any unit test suite I’ve ever written.
- Use coverage as a smell detector, not a gate. If a PR drops coverage by 30%, that’s worth discussing. But a hard threshold creates perverse incentives.
Still wrestling with
The counter-argument I haven’t fully resolved: new developers on a codebase use existing tests as documentation. Under-tested code is harder to onboard onto. There’s probably a middle ground I haven’t found yet.