Skip to main content

Vitest Coverage Reports: Configuration & Analysis

Code coverage measures how much of your codebase is tested—what percentage of lines, branches, functions, and statements your tests actually execute. A high coverage number does not guarantee quality tests, but low coverage indicates untested code paths where bugs hide. Vitest generates coverage reports instantly and can enforce coverage thresholds to prevent regressions (Vitest documentation, 2026). This article teaches you to configure coverage, interpret reports, and use them to improve test quality.

Understanding Coverage Metrics

Coverage has four main metrics:

MetricMeaningExample
LinePercentage of executable lines coveredIf file has 100 lines, 80 are executed by tests → 80% coverage
StatementPercentage of statements coveredSimilar to line; multiple statements per line count separately
BranchPercentage of conditional branches coveredif (x) { a } else { b } has 2 branches; both must execute for 100%
FunctionPercentage of defined functions coveredIf you define 10 functions, tests must call all 10 for 100%

Example file and its coverage:

export function grade(score: number): string {
if (score >= 90) {
return 'A';
} else if (score >= 80) {
return 'B'; // ❌ Untested branch
} else {
return 'F';
}
}

export function bonus(): void {
console.log('Bonus!'); // ❌ Untested function
}

If you only test grade(95) and grade(70):

  • Line coverage: 83% (5 of 6 lines executed).
  • Branch coverage: 67% (2 of 3 branches covered; score >= 80 is untested).
  • Function coverage: 50% (only grade tested, bonus not called).

Install and Configure Coverage

Install the coverage reporter:

npm install -D @vitest/coverage-v8

Update vitest.config.ts:

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
coverage: {
provider: 'v8', // Use V8 coverage (fast, accurate)
reporter: ['text', 'html', 'json'], // Output formats
reportsDirectory: './coverage', // Output directory
exclude: [
'node_modules/',
'dist/',
'**/*.test.ts',
'**/*.test.tsx',
'**/index.ts', // Export files typically untested
],
lines: 80, // Minimum 80% line coverage required
functions: 80,
branches: 75, // Branches are hardest; slightly lower threshold
statements: 80,
},
},
});

Generate Coverage Reports

Run tests with coverage:

npm test -- --coverage

Output includes a text summary and (optionally) an HTML report:

File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files | 82.5 | 78.2 | 88.1 | 82.1 |
Button | 100 | 100 | 100 | 100 |
Form | 75 | 60 | 85 | 72 |
Modal | 90 | 85 | 100 | 92 |

Open the HTML report (most useful):

open coverage/index.html # macOS
start coverage/index.html # Windows

The HTML report is interactive—click files to see which lines are untested (highlighted in red).

Enforce Coverage Thresholds

Configure minimum coverage requirements. If thresholds aren't met, tests fail:

// vitest.config.ts
coverage: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
}

Now if a file falls below 80% line coverage, the test suite exits with error code 1 (fails in CI).

You can also enforce thresholds per-file:

coverage: {
perFile: true,
lines: 85, // Every file must have 85%+ coverage
all: true, // Include all files, not just tested ones
}

Interpreting Coverage Reports

Let's look at a real example:

// Button.tsx
export function Button({ label, onClick, disabled }: Props) {
if (disabled) {
return <button disabled>{label}</button>;
}

return <button onClick={onClick}>{label}</button>;
}

export function PrimaryButton(props: Props) {
return <Button {...props} />;
}

If your test only covers the enabled state:

it('renders with label', () => {
render(<Button label="Click me" onClick={vi.fn()} />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

Coverage report:

  • Line coverage: 80% (4 of 5 lines covered; disabled return untested).
  • Branch coverage: 50% (only enabled branch tested).
  • Function coverage: 50% (PrimaryButton not tested).

To fix these gaps:

it('renders disabled state', () => {
render(<Button label="Click" disabled />);
expect(screen.getByRole('button')).toHaveAttribute('disabled');
});

it('renders through PrimaryButton wrapper', () => {
render(<PrimaryButton label="Click" onClick={vi.fn()} />);
expect(screen.getByText('Click')).toBeInTheDocument();
});

Now all metrics hit 100%.

Coverage Reports in CI/CD

Add coverage check to your CI pipeline:

# .github/workflows/test.yml
- name: Run tests with coverage
run: npm test -- --coverage

# If coverage is below threshold, this step fails
- name: Upload coverage report
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json

Interpreting "Not Covered" Lines

The HTML report colors lines:

  • Green: Tested (executed).
  • Red: Not tested (not executed).
  • Yellow: Partially covered (some branches not tested).

Example:

if (score >= 90) {
return 'A'; // Green (tested)
} else if (score >= 80) {
return 'B'; // Yellow (tested as the else, but not the condition)
} else {
return 'F'; // Green (tested)
}

To cover the yellow branch, add a test for score === 85.

Excluding Files from Coverage

Exclude files that shouldn't be tested (stubs, index re-exports, etc.):

// vitest.config.ts
coverage: {
exclude: [
'node_modules/',
'dist/',
'src/index.ts', // Pure re-exports
'src/**/*.test.ts', // Test files themselves
'src/**/__mocks__/**', // Mock modules
'src/types/**', // TypeScript type-only files
],
}

Or ignore specific lines with comments:

/* v8 ignore next 3 */ // Ignore next 3 lines
export function errorHandler(err: Error) {
console.log(err); // Rarely executed in tests
}

Coverage Tips and Gotchas

Don't obsess over coverage numbers. 80% coverage of meaningful tests beats 100% coverage of trivial tests. Focus on covering:

  • Happy path (component works correctly).
  • Error cases (validation, network failures).
  • Edge cases (empty lists, null values).

Branch coverage is hardest. 100% line coverage is easy (just call the function). 100% branch coverage is hard (test every if path). Set branch thresholds slightly lower (75% vs. 80%).

Untested doesn't mean bad. Some code is legitimately hard to test (browser APIs, timing-dependent code). Use /* v8 ignore */ comments judiciously instead of writing brittle tests.

Keep thresholds realistic. 80% is a good default. 90%+ requires significant effort and may not be worth the time. Focus on critical paths.

Key Takeaways

  • Four coverage metrics: line, statement, branch, and function.
  • Install @vitest/coverage-v8 and add coverage config to vitest.config.ts.
  • Run npm test -- --coverage to generate reports (HTML is most useful).
  • Set thresholds in config to enforce minimum coverage and prevent regressions.
  • Use the HTML report to identify untested lines (red) and branches (yellow).
  • Focus on meaningful coverage (happy path, errors, edge cases) not just numbers.

Frequently Asked Questions

What's a good coverage threshold?

Aim for 80% across the board (lines, functions, statements). Set branches to 75% (branches are harder). Don't go above 90% unless you have time and resources—the ROI diminishes quickly. For critical paths (auth, payments), aim higher (90%+).

Why do some lines show as not covered even though I ran the code?

Dead code optimization. Modern JavaScript bundlers eliminate unreachable code, so if a line can never execute (e.g., after a throw statement), coverage reporters mark it untested. Add /* v8 ignore */ comments to silence the warning.

How do I test error paths I can't easily trigger?

Use mocks or condition mocking:

// Mock a function to throw an error
const mockFn = vi.fn(() => {
throw new Error('API failed');
});

// Now test the error handler

Or use condition mocking to force a path:

if (process.env.TEST_ERROR_PATH === 'true') {
throw new Error('Testing error');
}

Can I exclude a single function from coverage without excluding the whole file?

Yes, use a comment:

/* v8 ignore next */ // Ignore next function
export function neverCalled() {
console.log('test');
}

Why does my HTML report show 0% coverage?

Ensure provider: 'v8' is in your coverage config. Also check that reporter includes 'html'. If still broken, clear cache: rm -rf ./coverage && npm test -- --coverage.

How do I upload coverage reports to codecov or coveralls?

Use their GitHub Actions:

- run: npm test -- --coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json

Or manually with curl:

bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json

Further Reading