Skip to content
Back to Quality Assurance

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 instead

Example

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