"Coverage tells you what you didn't test. It can't tell you whether you tested well."
Why v8 (not Istanbul) by Default
Vitest supports two coverage providers: v8 (native to the V8 engine) and istanbul (source-instrumented). v8 is the default and usually the right choice — it's faster (no source rewriting), more accurate for ESM/async code, and works without TypeScript / Vite plugin dances. Use Istanbul only when you need its specific instrumentation features (e.g., merging coverage across multiple runtimes).
Install: npm install -D @vitest/coverage-v8. Then enable in config or via CLI: npx vitest run --coverage.
Four Numbers, Not Five
Coverage reports show four percentages:
Statements: what fraction of executable statements ran.
Branches: what fraction of conditional branches (if/else, ternary) ran.
Functions: what fraction of functions got called.
Lines: what fraction of lines ran (similar to statements but at line granularity).
Branches is the most informative. A high statement % with low branch % means your tests hit the happy path but skip the error/edge branches. The branch number is usually where the bugs are hiding.
What Coverage Does NOT Measure
Coverage does not measure assertion quality. A test that calls a function and asserts nothing counts the same as a test that asserts thirty things. it('does the thing', () => { doTheThing(); }) is full coverage on that function and zero verification. Use coverage to find untested code paths, not to claim tested code is correct.
Coverage also doesn't measure test-suite usefulness. A 100%-coverage suite that doesn't catch the bugs you actually ship is worse than a 60%-coverage suite that catches them. The number is a tool, not a goal.
Coverage is a diagnostic, not a target. 'We will hit 90% coverage by Q3' is a doomed goal — teams hit it by writing assertion-free tests or by excluding the hard files. The useful goal is 'every critical path has at least one test' — coverage helps find what's missing, not what's enough.
Thresholds — Use Them as Floors, Not Ceilings
You can set coverage.thresholds in vitest.config to fail the run if coverage drops below a number. The right way to use this: set it as a floor that matches your current actual coverage, ratchet it up only when real improvements happen. The wrong way: set 80% across the board on a 40%-coverage suite, then watch the team game it.
Excluding Code That Shouldn't Count
Some files genuinely should not contribute to coverage: type-only files, generated code, dev-only utilities, the test files themselves. Configure coverage.exclude to drop them — the percentages get more meaningful when noise is gone.
Code
Install + run with coverage·bash
# Install the v8 provider
npm install -D @vitest/coverage-v8
# Run with coverage
npx vitest run --coverage
# Coverage UI (alongside the regular vitest UI)
npx vitest --coverage --ui
Enable coverage on your project (@vitest/coverage-v8 + config block). Run npx vitest run --coverage and open the generated coverage/index.html. Pick the file with the LOWEST branch coverage — open it, find one untested branch, add a test for it. Re-run, watch the number tick up. Then set a per-file threshold for that file at the current coverage so it can't regress.
Hint
If a file shows 0% coverage, check that it's actually imported by something the tests reach. Dead code shows as 0%; that's a hint to delete the file, not write tests for it.
Progress
Progress is local-only — sign in to sync across devices.