Code Coverage & Quality Gates
What coverage actually measures — line vs branch coverage, why 100% doesn't mean correct, and how to use thresholds as a guardrail instead of a vanity metric.
Code coverage measures which parts of your code your tests execute — lines, statements, branches, functions. It's a useful map of untested areas, with branch coverage being far more meaningful than line coverage. But coverage shows only that code ran, not that you asserted the right things, so it's a guardrail to prevent regression, not a target that proves quality.
Code coverage tools run your tests while recording which parts of the code executed. They report metrics like line coverage (lines run), statement, function, and branch coverage (which decision outcomes — both the if and the else — ran). It's an objective map of what your tests touched.
Line coverage is the headline number but the weakest: a test can run every line yet only ever take one side of each if. Branch (decision) coverage is stricter — it requires every conditional outcome to be exercised. 100% line with 60% branch means half your decision logic is untested.
Coverage tells you what code ran, not whether your assertions were meaningful. You can hit 100% coverage with tests that assert nothing useful — executing code without verifying its behavior. High coverage on bad tests gives false confidence; it's a floor, not proof of quality.
The healthy use: set a reasonable threshold (often branch coverage) as a CI guardrail to prevent coverage from regressing, focus coverage on important logic, and exclude generated or trivial code. Treat low coverage as a signal to investigate — and never treat a high number as a goal in itself (Goodhart's Law).
Key Concepts
Percentage of code lines executed by tests. The most common but weakest metric.
Percentage of decision outcomes (both if and else) exercised. Stricter and more meaningful.
Other granularities coverage tools report — statements run, functions called.
Coverage shows code ran, not that assertions were meaningful — you can have 100% with useless tests.
A CI gate (e.g. branches ≥ 80%) that prevents coverage from regressing over time.
When a measure becomes a target it stops being a good measure — gaming coverage produces hollow tests.
1// This single test gives 100% LINE coverage of a 1-line function...2function isAdult(age) { return age >= 18; }3test("adult", () => { expect(isAdult(20)).toBe(true); }); // line: 100% ✅45// ...but never checks the boundary or the false branch. The bug below6// (>= vs >) would pass with 100% line coverage:7function isAdult(age) { return age > 18; } // 🐛 18 now returns false8// → you need branch + boundary tests, not just a green coverage number.910// Coverage as a guardrail (don't let it DROP), not a vanity target:11// jest --coverage --coverageThreshold='{"global":{"branches":80}}'12// Track BRANCH coverage (stricter than line) and exclude generated code.
Coverage is ubiquitous in CI and a classic interview 'gotcha' (does 100% coverage mean bug-free? — no). Understanding the difference between line and branch coverage, and why a high number can hide weak tests, lets you use coverage wisely — as a signal for gaps and a regression guardrail — rather than chasing a misleading score.
Common Pitfalls
1The 95% Coverage That Caught Nothing
A team proudly maintained 95% line coverage, yet bugs kept reaching production. Reviewing the tests revealed many executed code paths but barely asserted anything — they ran the code and checked it 'didn't throw'.
Coverage was treated as the goal, so tests were written to touch lines, not to verify behavior. High line coverage masked weak assertions and ignored branch/edge cases entirely — a textbook false sense of security.
Shift the metric to branch coverage as a guardrail, and review tests for meaningful assertions (correct outputs, error cases, boundaries) rather than execution. Add tests for the untested branches and edge cases the number had hidden. Coverage becomes a signal, not a scoreboard.
Takeaway: A high coverage number doesn't mean good tests. Measure branch coverage, insist on meaningful assertions, and treat coverage as a guardrail that flags gaps — never as proof of quality.
2Coverage Silently Eroding on a Big Codebase
On a large, fast-moving repo, new code was often shipped with few or no tests. There was no signal until coverage had quietly slid from 80% to 55% and the suite no longer protected critical paths.
Without a gate, coverage decays as features outpace tests. The erosion is invisible day-to-day and only noticed once the safety net has already thinned dramatically.
Add a CI coverage threshold (e.g. fail the build if branch coverage drops below the current level or a set floor), reported per-PR so authors see the impact of their change. New code must come with tests to keep the build green, halting the slow decline.
Takeaway: Use coverage thresholds in CI as a ratchet that prevents regression, surfaced per-PR. The point isn't a perfect number — it's stopping the safety net from silently unraveling as the codebase grows.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.