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=json

Package.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#

  1. Configure coverage: Add coverage settings to vitest.config.ts
  2. Set thresholds: Define minimum coverage requirements
  3. Run coverage: Use npm test -- --coverage
  4. Review reports: Check HTML report for detailed line coverage
  5. Integrate with CI: Add coverage gates to your pipeline

Best Practices#

  1. Focus on meaningful coverage - High coverage doesn't mean good tests
  2. Test critical paths thoroughly - Payment, auth, and data integrity
  3. Use branch coverage - Ensure all code paths are tested
  4. Set realistic thresholds - 80% is a good starting point
  5. Ignore appropriately - Don't ignore to hide poor coverage
  6. Track trends - Monitor coverage over time
  7. Review uncovered code - Understand why code isn't covered