Coverage Strategy
Use coverage as a map of untested risk, not as a percentage to maximize.
Why
Coverage is the most misunderstood metric in testing. 100% line coverage does not mean 100% tested — it means every line executed at least once, not that every line was asserted on. A suite with 100% coverage and zero assertions passes green and guards nothing. The value of coverage is not the number; it is the map it produces: the uncovered branches in business logic are the places where a bug can hide uncaught. Used as a risk map, coverage guides where to write the next test. Used as a target, it incentivizes trivial tests that tick the counter without verifying behavior, and the number becomes a lie.
When
Generate a coverage report on every CI run and review it when writing new code or planning a test sprint. Do not gate merges on a coverage threshold — gate on whether the new code's uncovered branches are in business logic (must cover) or in trivial code (acceptable).
How
1. Configure coverage collection in your test runner
- Vitest: coverage: { provider: "v8", reporter: ["text", "html", "lcov"] }
- Include only src/ (business logic), exclude tests/, config, entry points
- Use branch coverage, not just line coverage — branches catch the if/else gaps
2. Generate the report and read the HTML view
- Open coverage/index.html — red lines are uncovered, yellow are partial branches
- Focus on business-logic files (src/lib, src/services), not on config or UI shells
- Note the specific uncovered branches, not just the percentage
3. Classify each uncovered branch
- "Must cover" — business logic, error handling, boundary conditions → write a test
- "Should cover" — less critical but still logic → write a test if time allows
- "Acceptable" — trivial getters, framework boilerplate, error-logging-only → skip
4. Write tests for the "must cover" branches, not to raise the percentage
- The goal is to eliminate the risk, not to tick a counter
- A test that covers a branch without asserting on it is worthless — verify behavior
5. Track coverage trend, not absolute number
- A drop in coverage on changed files is a signal someone skipped tests
- A rise from trivial-test padding is not improvement — look at mutation score insteadExample
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
include: ["src/lib/**", "src/services/**"], // business logic only
exclude: ["**/*.config.*", "**/index.ts", "**/*.test.ts"],
branches: 100, // report all uncovered branches
lines: 80, // warn if business-logic line coverage < 80%
},
},
});
// After a test run, open coverage/index.html:
// src/lib/discount.ts — 100% lines, 75% branches (the "else" of SAVE20 untested)
// src/lib/pricing.ts — 60% lines (the bulk-discount branch never exercised)
// src/services/checkout.ts — 90% lines, 50% branches (error paths untested)
//
// Action: write tests for checkout.ts error paths and pricing.ts bulk-discount.
// Do NOT write a test for discount.ts just to hit 100% — the uncovered branch
// is a trivial else-return, and a test would add noise without catching a bug.Common Problems
- ⚠Setting a hard 80% coverage gate on every file incentivizes developers to write trivial tests for boilerplate code (getters, framework hooks) just to pass the gate, adding noise without reducing risk.
- ⚠Reporting only line coverage hides the most important gaps — a function where every line runs but the if and else branches are never both exercised has 100% line coverage and 50% branch coverage.
- ⚠Chasing 100% coverage on the entire repo (including config, entry points, type definitions) wastes effort on code that cannot meaningfully be tested and dilutes focus from business logic.
- ⚠Using coverage as the sole quality metric leads to suites with high coverage and low assertion quality — the number looks great, the suite catches nothing, and nobody notices until production.
Best Practices
- ✓Scope coverage to business logic (src/lib, src/services) and exclude config, entry points, and type definitions — the number that matters is coverage on code that can have bugs.
- ✓Use branch coverage as the primary metric, not line coverage — branches reveal the if/else and switch gaps where boundary bugs hide, and line coverage hides them.
- ✓Review the uncovered-branch report as a risk map: each red branch in business logic is a place a bug can hide, and the next test should target the highest-risk red branch.
- ✓Pair coverage with mutation testing for a true quality signal — coverage says "this line ran," mutation says "the test would notice if this line were wrong," and only the latter means tested.