Coverage Pattern
Set up and use code coverage with Vitest, including configuration, thresholds, CI integration, and best practices for meaningful coverage.
Overview#
Code coverage measures how much of your code is executed during tests. It helps identify untested code paths and ensures critical functionality is covered.
When to use:
- Quality gates in CI/CD pipelines
- Identifying untested code
- Tracking coverage trends over time
- Validating refactoring doesn't reduce coverage
Key features:
- Line, branch, function, and statement coverage
- Per-file and global thresholds
- HTML, JSON, and text reports
- CI integration with coverage gates
Code Example#
Vitest Coverage Setup#
1// vitest.config.ts
2import { defineConfig } from 'vitest/config'
3
4export default defineConfig({
5 test: {
6 coverage: {
7 provider: 'v8',
8 reporter: ['text', 'json', 'html', 'lcov'],
9 exclude: [
10 'node_modules/',
11 'tests/',
12 '**/*.d.ts',
13 '**/*.config.ts',
14 '**/types.ts',
15 '**/index.ts', // barrel files
16 '**/*.stories.tsx' // Storybook
17 ],
18 include: ['src/**/*.{ts,tsx}'],
19 thresholds: {
20 lines: 80,
21 functions: 80,
22 branches: 80,
23 statements: 80
24 }
25 }
26 }
27})Run with Coverage#
1# Generate coverage report
2npm run test -- --coverage
3
4# Watch mode with coverage
5npm run test -- --coverage --watch
6
7# Coverage for specific files
8npm run test -- --coverage src/lib/
9
10# Generate only specific reporters
11npm run test -- --coverage --reporter=jsonPackage.json Scripts#
1{
2 "scripts": {
3 "test": "vitest",
4 "test:coverage": "vitest --coverage",
5 "test:ci": "vitest --coverage --reporter=json --reporter=default"
6 }
7}Coverage Thresholds#
1// vitest.config.ts - Per-file thresholds
2export default defineConfig({
3 test: {
4 coverage: {
5 thresholds: {
6 // Global thresholds
7 lines: 80,
8 branches: 75,
9 functions: 80,
10 statements: 80,
11
12 // Per-glob thresholds
13 'src/lib/**/*.ts': {
14 lines: 90,
15 functions: 90
16 },
17 'src/utils/**/*.ts': {
18 lines: 95,
19 functions: 100
20 }
21 }
22 }
23 }
24})
25
26// Or per-file thresholds
27export default defineConfig({
28 test: {
29 coverage: {
30 thresholds: {
31 // Strict coverage for critical files
32 'src/lib/auth.ts': {
33 lines: 100,
34 functions: 100
35 },
36 'src/lib/payments.ts': {
37 lines: 95,
38 branches: 90
39 }
40 }
41 }
42 }
43})Istanbul Ignore Comments#
1// Ignore specific lines
2/* istanbul ignore next */
3if (process.env.NODE_ENV === 'development') {
4 console.log('Debug mode')
5}
6
7// Ignore entire function
8/* istanbul ignore next */
9function devOnlyHelper() {
10 // This won't affect coverage
11}
12
13// Ignore else branch
14if (condition) {
15 doSomething()
16} /* istanbul ignore else */ else {
17 // Edge case that's hard to test
18}
19
20// Ignore specific condition
21const value = /* istanbul ignore next */ fallback ?? defaultValue
22
23// V8 specific ignore
24/* v8 ignore next */
25unreachableCode()
26
27/* v8 ignore start */
28// Block of code to ignore
29/* v8 ignore stop */Critical Path Coverage#
1// Ensure critical paths are always tested
2// tests/critical-paths.test.ts
3
4describe('Critical Paths', () => {
5 describe('Authentication', () => {
6 it('handles login success', async () => {
7 // Test successful login flow
8 })
9
10 it('handles login failure with wrong password', async () => {
11 // Test password validation
12 })
13
14 it('handles login failure with unknown email', async () => {
15 // Test email validation
16 })
17
18 it('handles session expiry', async () => {
19 // Test expired session handling
20 })
21
22 it('handles logout', async () => {
23 // Test logout flow
24 })
25 })
26
27 describe('Payment Processing', () => {
28 it('handles successful payment', async () => {
29 // Test successful payment
30 })
31
32 it('handles declined card', async () => {
33 // Test card decline
34 })
35
36 it('handles webhook verification', async () => {
37 // Test webhook signature validation
38 })
39
40 it('handles refunds', async () => {
41 // Test refund processing
42 })
43 })
44
45 describe('Data Integrity', () => {
46 it('validates user input', async () => {
47 // Test input validation
48 })
49
50 it('sanitizes output', async () => {
51 // Test output sanitization
52 })
53
54 it('handles concurrent updates', async () => {
55 // Test race conditions
56 })
57 })
58})Branch Coverage#
1// Ensure all branches are tested
2function getDiscount(user: User, order: Order): number {
3 // Branch 1: Premium user
4 if (user.isPremium) {
5 // Branch 1a: Large order
6 if (order.total > 100) {
7 return 0.2
8 }
9 // Branch 1b: Small order
10 return 0.1
11 }
12
13 // Branch 2: Regular user with large order
14 if (order.total > 200) {
15 return 0.05
16 }
17
18 // Branch 3: No discount
19 return 0
20}
21
22// Tests for all branches
23describe('getDiscount', () => {
24 it('returns 20% for premium user with large order', () => {
25 const user = { isPremium: true }
26 const order = { total: 150 }
27 expect(getDiscount(user, order)).toBe(0.2)
28 })
29
30 it('returns 10% for premium user with small order', () => {
31 const user = { isPremium: true }
32 const order = { total: 50 }
33 expect(getDiscount(user, order)).toBe(0.1)
34 })
35
36 it('returns 5% for regular user with large order', () => {
37 const user = { isPremium: false }
38 const order = { total: 250 }
39 expect(getDiscount(user, order)).toBe(0.05)
40 })
41
42 it('returns 0% for regular user with small order', () => {
43 const user = { isPremium: false }
44 const order = { total: 50 }
45 expect(getDiscount(user, order)).toBe(0)
46 })
47
48 // Edge cases
49 it('returns 10% for premium user at exactly $100', () => {
50 const user = { isPremium: true }
51 const order = { total: 100 }
52 expect(getDiscount(user, order)).toBe(0.1)
53 })
54
55 it('returns 5% for regular user at exactly $200', () => {
56 const user = { isPremium: false }
57 const order = { total: 200 }
58 expect(getDiscount(user, order)).toBe(0)
59 })
60})CI Coverage Gates#
1# .github/workflows/test.yml
2name: Test
3
4on: [push, pull_request]
5
6jobs:
7 test:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11 - uses: actions/setup-node@v4
12 with:
13 node-version: '20'
14 cache: 'npm'
15
16 - run: npm ci
17 - run: npm run test:ci
18
19 - name: Check coverage thresholds
20 run: |
21 COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
22 echo "Line coverage: $COVERAGE%"
23 if (( $(echo "$COVERAGE < 80" | bc -l) )); then
24 echo "Coverage $COVERAGE% is below 80% threshold"
25 exit 1
26 fi
27
28 - name: Upload coverage to Codecov
29 uses: codecov/codecov-action@v4
30 with:
31 files: ./coverage/lcov.info
32 fail_ci_if_error: true
33 token: ${{ secrets.CODECOV_TOKEN }}
34
35 - name: Comment coverage on PR
36 if: github.event_name == 'pull_request'
37 uses: actions/github-script@v7
38 with:
39 script: |
40 const fs = require('fs');
41 const coverage = JSON.parse(fs.readFileSync('coverage/coverage-summary.json'));
42 const lines = coverage.total.lines.pct.toFixed(2);
43 const branches = coverage.total.branches.pct.toFixed(2);
44
45 github.rest.issues.createComment({
46 issue_number: context.issue.number,
47 owner: context.repo.owner,
48 repo: context.repo.repo,
49 body: `## Coverage Report\n\n| Metric | Coverage |\n|--------|----------|\n| Lines | ${lines}% |\n| Branches | ${branches}% |`
50 });Coverage Report Analysis#
1// scripts/analyze-coverage.ts
2import fs from 'fs'
3
4interface CoverageMetric {
5 total: number
6 covered: number
7 skipped: number
8 pct: number
9}
10
11interface FileCoverage {
12 lines: CoverageMetric
13 functions: CoverageMetric
14 statements: CoverageMetric
15 branches: CoverageMetric
16}
17
18const summary = JSON.parse(
19 fs.readFileSync('coverage/coverage-summary.json', 'utf-8')
20)
21
22// Find files with low coverage
23const lowCoverage = Object.entries(summary)
24 .filter(([path]) => path !== 'total')
25 .filter(([, data]) => (data as FileCoverage).lines.pct < 80)
26 .sort(([, a], [, b]) =>
27 (a as FileCoverage).lines.pct - (b as FileCoverage).lines.pct
28 )
29
30console.log('Files with < 80% coverage:')
31lowCoverage.forEach(([path, data]) => {
32 const coverage = data as FileCoverage
33 console.log(` ${path}: ${coverage.lines.pct.toFixed(1)}%`)
34})
35
36// Find uncovered lines
37const detailed = JSON.parse(
38 fs.readFileSync('coverage/coverage-final.json', 'utf-8')
39)
40
41console.log('\nUncovered functions:')
42Object.entries(detailed).forEach(([file, data]: [string, any]) => {
43 const uncovered = Object.entries(data.fnMap)
44 .filter(([key]) => data.f[key] === 0)
45 .map(([, fn]: [string, any]) => fn.name)
46
47 if (uncovered.length > 0) {
48 console.log(` ${file}:`)
49 uncovered.forEach(name => console.log(` - ${name}`))
50 }
51})Usage Instructions#
- Configure coverage: Add coverage settings to vitest.config.ts
- Set thresholds: Define minimum coverage requirements
- Run coverage: Use
npm test -- --coverage - Review reports: Check HTML report for detailed line coverage
- Integrate with CI: Add coverage gates to your pipeline
Best Practices#
- Focus on meaningful coverage - High coverage doesn't mean good tests
- Test critical paths thoroughly - Payment, auth, and data integrity
- Use branch coverage - Ensure all code paths are tested
- Set realistic thresholds - 80% is a good starting point
- Ignore appropriately - Don't ignore to hide poor coverage
- Track trends - Monitor coverage over time
- Review uncovered code - Understand why code isn't covered
Related Patterns#
- Vitest - Test runner configuration
- Unit Testing - Unit testing patterns
- Integration Testing - Integration tests
- CI/CD - CI pipeline setup