Section 5 · Testing Strategy
Coverage and Its Limits
18 min3 min readCode playground
Lesson at a glance
Measure code coverage with Vitest, read the report to find untested code, and explain why a high coverage number can still hide a weak test suite.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Every team that adds tests eventually asks “how much is enough?”, and coverage is the number that seems to answer. It answers a narrower question than people think. Used well, it’s a fast way to find code nobody tested. Used as a score, it rewards the wrong tests.
Turning it on
Vitest supports two coverage providers. The default, v8, uses the JavaScript engine’s built-in coverage and needs no instrumentation step; istanbul instruments the code and works in more runtimes. Install the provider and run with --coverage:
npm install --save-dev @vitest/coverage-v8
npx vitest run --coverageConfigure what to measure and how to report it:
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.{js,jsx}'],
exclude: ['src/**/*.test.{js,jsx}', 'src/main.jsx'],
reporter: ['text', 'html'],
thresholds: { lines: 80, branches: 75 },
},
},
});include matters: in Vitest 4, without it, the report lists only files your tests happened to load, so a completely untested module doesn’t appear at all and the percentage looks better than it is. With include, untested files show up at 0%. The html reporter writes coverage/index.html, which you open in a browser.
The four numbers
- Statements: how many statements ran.
- Branches: how many sides of each decision ran (
if/else,? :,&&,||,??, default parameters). - Functions: how many functions were called at least once.
- Lines: how many lines had something run on them.
Branch coverage is the one I look at. const share = people > 0 ? total / people : 0; is one line and two branches. A test with three people makes the line 100% covered and the branches 50%, and the untested branch is exactly the edge case from section 2.
Reading the report
The HTML report marks lines that never ran in red and branches that were taken only one way in yellow. That’s the valuable part: red code is untested, guaranteed. No assertion anywhere could have checked it, because it never ran. When I join a project, the coverage report is the fastest map of where the risk lives: the uncovered error handling in the payment module, the else branch nobody hit.
What coverage can’t see
Coverage records execution. It has no idea whether anything checked the result. This test makes computeBalances 100% covered:
test('computes balances', () => {
const balances = computeBalances([
{ paidBy: 'Ana', amountCents: 3000, splitAmong: ['Ana', 'Ben', 'Cai'] },
]);
expect(Object.keys(balances)).toHaveLength(3);
});Every line ran. The assertion would still pass if the payer were debited instead of credited, if the shares were floored and a cent vanished, or if a second expense overwrote the first. Coverage: 100%. Confidence: close to zero. The exercise starts from exactly this test.
An uncovered line is a question, not an order. Sometimes the right answer is a test. Sometimes it’s deleting the code: a branch nobody can reach, a fallback for a browser you stopped supporting, a helper nothing calls any more. Dead code that is “covered” by a test written only to raise the number is the worst outcome, because now it looks important.
How much is enough?
There’s no universal number. Useful rules of thumb:
- Aim for high branch coverage on core logic (money, permissions, data transformations), and accept less on glue code and UI wiring that end-to-end tests exercise anyway.
- Exclude code you deliberately don’t unit test (entry points, generated files, type-only modules) so the number means something.
- Look at coverage on the diff in code review: did this change add untested branches? That’s more actionable than a project-wide percentage.
Coverage tells you where tests are missing. To learn whether the tests you have are any good, you need a different tool, which is the next lesson. First, in the exercise, turn a 100%-coverage, catches-nothing suite into one that catches real bugs.
Key takeaways
- Coverage measures which statements, branches, functions and lines ran during the tests, not whether anything checked the results.
- The most useful part of a coverage report is the red and yellow it shows you; uncovered code is untested code for certain.
- Branch coverage is stricter and more useful than line coverage, because one line can hold several decisions.
- Use thresholds as a floor that stops coverage from sliding, not as a target to hit; a number that becomes a goal gets gamed.