Back to updates
Thought

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.

testingengineering-culture

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:

  1. 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.

  2. 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.

  3. 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.