C.W.K.
Stream
Lesson 01 of 03 · published

Coverage with v8 — Measure What Matters

~16 min · vitest-advanced, coverage, v8

Level 0Test Curious
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"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
Coverage config — providers, reporters, exclusions, thresholds·typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'happy-dom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'json-summary', 'lcov'],
      reportsDirectory: './coverage',
      include: ['src/**/*.{ts,tsx}'],
      exclude: [
        'src/**/*.test.{ts,tsx}',
        'src/**/*.spec.{ts,tsx}',
        'src/**/__mocks__/**',
        'src/**/*.d.ts',
        'src/types/**',                 // type-only
        'src/main.tsx',                 // bootstrap, no logic
      ],
      thresholds: {
        // Floor — fail CI if any number drops below.
        statements: 70,
        branches: 65,
        functions: 70,
        lines: 70,
        // Per-file floor for critical paths
        'src/services/payment.ts': {
          statements: 95,
          branches: 90,
        },
      },
    },
  },
});
What a coverage report actually looks like·text
% Statements   : 78.34 ( 245/313 )
% Branches     : 71.42 (  85/119 )
% Functions    : 81.25 (  52/64  )
% Lines        : 79.16 ( 247/312 )

File                          | % Stmts | % Branch | % Funcs | % Lines |
------------------------------|---------|----------|---------|---------|
All files                     |   78.34 |    71.42 |   81.25 |   79.16 |
 src/lib                      |   95.12 |    88.88 |  100.00 |   95.12 |
  format.ts                   |  100.00 |   100.00 |  100.00 |  100.00 |
  parse.ts                    |   88.88 |    75.00 |  100.00 |   88.88 |
 src/services                 |   62.45 |    54.12 |   71.42 |   63.10 |  ← needs work
  payment.ts                  |   42.18 |    28.57 |   50.00 |   42.18 |  ← critical, low
  notifications.ts            |   90.00 |    85.71 |  100.00 |   90.00 |
Inline ignore — for the rare lines that genuinely don't need coverage·typescript
// Per-line / per-block exclusion when you genuinely cannot test something
// (e.g., browser-only globals in Node, hardware paths)

export function getPlatform() {
  /* v8 ignore next 3 */
  if (typeof window === 'undefined') {
    return 'node';
  }
  return 'browser';
}

export function bootCriticalSystem() {
  /* v8 ignore start */
  if (process.env.SKIP_BOOT === 'true') {
    return;   // dev escape hatch — not part of production logic
  }
  /* v8 ignore stop */

  initializeWidgets();
}

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.