Coverage Metrics Pitfalls
Why 100% line coverage can still ship bugs — gaming metrics, useless tests, and what coverage is actually good for.
- testing
- coverage-metrics
Coverage tools answer: which lines/branches ran during tests? They do not answer: are the assertions meaningful? Chasing 100% creates cargo-cult tests that execute code without checking behavior.
Docs: Istanbul/nyc, V8 coverage in Vitest/Jest.
What the numbers mean
| Metric | Measures |
|---|---|
| Line | Statements executed |
| Branch | if/?:/&& paths |
| Function | Functions entered |
| Statement | Similar to line (tool-dependent) |
Branch coverage is usually more honest than line coverage for UI logic.
Classic gaming patterns
// Executes code, asserts nothing useful
test('renders', () => {
render(<Page />);
});
// Forces branch without checking outcome
test('error branch', () => {
render(<Page fail />);
// no expect
});
Coverage goes up. Confidence doesn’t.
False confidence examples
- Snapshots of entire trees — any change updates the snap; bugs “expected.”
- Mocked everything — test verifies mock configuration.
- Untested catch blocks with empty handlers that still “run.”
- CSS/UI states not represented as branches in JS — uncovered UX.
What coverage is good for
- Finding untested modules after a feature lands.
- PR diffs: “new file at 0%” is a smell.
- Critical paths (auth, payments, permissions) with high branch coverage goals.
- Dead code hunts when combined with other signals.
vitest run --coverage
Read uncovered lines in the HTML report — don’t only watch the badge percentage.
Reasonable policy
| Area | Policy |
|---|---|
| Design system primitives | High branch coverage + a11y tests |
| Glue / generated code | Exclude from totals |
| Critical money paths | High coverage + E2E |
| Experimental pages | Don’t block on global 90% |
Global thresholds (e.g. 80%) can work if not gamed and exclusions are reviewed.
// vitest config sketch
"coverage": {
"thresholds": { "branches": 70, "lines": 70 },
"exclude": ["**/*.stories.*", "**/generated/**"]
}
Mutation testing (next level)
Tools like Stryker change code and check if tests fail. If coverage is high but mutants survive, assertions are weak. Expensive — use on core packages.
Footguns
- Counting stories/E2E toward unit coverage inconsistently.
- Excluding hard files instead of testing them.
- Failing CI on 0.1% drops from line shuffling.
- No review of what’s uncovered in security-sensitive code.
Interview out-loud answer
“Coverage shows what ran, not what’s correct. I use it to find gaps and protect critical branches, not as a vanity 100% goal. I watch for tests without assertions and prefer behavior checks with Testing Library over line-chasing.”
Diff coverage
Prefer gates on new lines in a PR (diff coverage) over global percentages that punish legacy. A 40% global codebase can still require 80%+ on touched lines — that drives behavior without fake tests on dead modules.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Related on this site
- Testing pyramid for frontend
- Unit testing pure logic
- Snapshot testing when useful
- Testing Library
- Flaky tests common causes
Further reading
Related guides
- Accessibility Testing in CIWire axe and lint rules into CI without false confidence — what automation catches, what it misses, and a practical pipeline.
- Component Testing StorybookUse Storybook as a component workshop and optional test runner — stories as living specs, interaction tests, and a11y checks.
- Contract Testing APIsKeep frontend and backend agreements honest — schema contracts, Pact-style consumer tests, and OpenAPI-driven checks without brittle E2E.
- End-to-End Testing TradeoffsWhen E2E tests earn their keep, why they flake, and how to keep a small critical-path suite instead of a slow second frontend.
- Fake Timers for DebounceTest debounced and throttled UI with Vitest/Jest fake timers — advance time deterministically without real sleeps.