What Code Coverage Percentage Should You Actually Aim For?
There is no universally correct code coverage percentage, and any consultant who gives you one without seeing your codebase is selling certainty they don't have. **The right target is whatever ensures your important code paths are tested — coverage is a useful diagnostic, but a dangerous goal.** Chasing a number rather than chasing confidence is how teams end up with 90% coverage and bugs in production.
## What Coverage Does and Doesn't Tell You
Code coverage measures which lines, branches, or conditions were executed during your tests. That is genuinely useful: it shows you code that *no test touches at all*, which is code you have zero automated confidence in.
But coverage does not measure whether your tests **assert anything meaningful**. A test that calls a function and checks nothing about the result will happily mark every line as covered while verifying nothing. High coverage tells you code *ran*; it says nothing about whether the right behaviour was confirmed.
This is the central trap: coverage is a measure of test *execution*, not test *quality*.
## Why a Hard Target Backfires
Goodhart's law applies brutally here: when a measure becomes a target, it stops being a good measure. Mandate 90% coverage and you will get 90% coverage — but not necessarily 90% confidence. Teams under a coverage mandate tend to:
- Write **assertion-free tests** that exercise code without checking outcomes.
- Add tests for **trivial code** (getters, constructors, generated boilerplate) because it is easy to cover, while leaving complex logic under-tested.
- **Delete or exclude** hard-to-test code from the coverage calculation rather than testing it.
- Treat hitting the number as "done", stopping exactly where the interesting edge cases begin.
The result is a suite that is expensive to maintain and still misses real bugs.
## Aim for Coverage of Risk
Instead of a blanket percentage, target coverage where it matters:
1. **Critical business logic** — pricing, permissions, payments, anything where a bug is expensive — should approach full branch coverage, including edge cases and error paths.
2. **Complex or frequently changed code** earns more tests than stable, simple code.
3. **Trivial code** (pass-through accessors, framework glue) deserves little or none; testing it inflates the number without adding confidence.
A codebase at 70% coverage with the dangerous 70% well-tested is in better shape than one at 90% where the critical paths are the untested 10%.
## Better Signals Than a Single Number
If you want metrics that resist gaming, look beyond a coverage percentage:
- **Branch and condition coverage**, not just line coverage — they reveal untested decision paths a line metric hides.
- **Coverage on changed lines** in each pull request, so new code is held to a standard without forcing a retrofit of legacy code.
- **Mutation testing**, which deliberately introduces bugs and checks whether your tests catch them. It directly measures assertion quality and is the strongest answer to assertion-free tests, though it is computationally expensive.
- **Escaped-defect rate** — how many bugs reach production. This is the outcome coverage is meant to influence.
## How to Use Coverage in CI
Coverage still earns a place in your pipeline if used as a guardrail rather than a goal:
- **Fail on coverage drops** in changed code rather than enforcing a global floor — this stops regressions without punishing inherited gaps.
- **Report, don't always block, the global number** so it informs without inviting gaming.
- **Pair it with mutation testing** on your most critical modules, where the cost is justified by the risk.
At neart.ai, building enterprise-grade products has taught us that confidence comes from testing the right things well, not from a green number on a dashboard.
## Practical Takeaway
Stop hunting for the perfect coverage percentage. Use coverage to find code with no tests at all, hold new code to a coverage standard in CI, and reserve near-complete coverage for critical business logic. Then verify that your tests actually assert something — ideally with mutation testing on your highest-risk modules — because executed code is not the same as verified behaviour.