AI-Assisted Testing & Tools

Code Coverage and Quality Metrics

Understand what code coverage measures, how to use it effectively, and what other metrics reveal test quality.

What Code Coverage Measures

Code coverage measures what percentage of your source code is executed when your tests run.

Four types of coverage:

Line coverage — Which lines of code were executed?

Branch coverage — Which branches (if/else, ternary) were taken?

Function coverage — Which functions were called?

Statement coverage — Which statements executed?

Branch coverage is the most meaningful — it catches logic that is only wrong in certain conditions.

Setting Up Coverage in Vitest

typescript
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: [
        'node_modules/**',
        'src/test/**',
        '**/*.config.ts',
        '**/*.d.ts',
      ],
      thresholds: {
        lines: 80,
        branches: 70,
        functions: 80,
        statements: 80,
      },
    },
  },
});
bash
npx vitest --coverage

This generates an HTML report at coverage/index.html. Red lines were never executed; yellow branches were partially covered.

The Coverage Trap

100% coverage does not mean zero bugs.

Coverage measures execution, not correctness. A test that calls a function and asserts nothing will count as coverage.

typescript
// This test achieves 100% line coverage of add()
it('covers add', () => {
  add(2, 3); // Executes every line
  // No assertion — verifies nothing
});

// This is the same coverage but actually tests behavior:
it('adds two numbers', () => {
  expect(add(2, 3)).toBe(5);
  expect(add(-1, 1)).toBe(0);
});

Coverage is a floor, not a ceiling. Use it to find gaps, not as a quality guarantee.

Practical Coverage Targets

Code TypeTarget Coverage
Auth, payments, data mutations95%+
Core business logic90%+
API routes85%+
UI components70%+
Configuration filesSkip
Generated codeSkip

Pursue 100% coverage in your most critical, high-risk code. Accept lower coverage in UI and exploratory code.

Mutation Testing: Coverage Done Right

Mutation testing answers: "If I introduce a bug, does a test fail?"

It works by automatically introducing small bugs (mutations) — changing > to >=, removing a condition, negating a boolean — and running your tests. If tests still pass with a bug present, they're not actually catching that bug.

bash
# Stryker is the leading mutation testing tool
npm install -D @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker run

Mutation score is a better quality metric than line coverage.

Key Takeaways

  • Branch coverage is more meaningful than line coverage — it catches conditional logic bugs
  • Set coverage thresholds that fail CI if coverage drops — this creates accountability
  • 100% coverage does not mean zero bugs — coverage measures execution, not correctness
  • Focus high coverage (95%+) on critical paths: authentication, payments, data mutations
  • Mutation testing is the gold standard — it verifies that your tests would actually catch bugs

Example

typescript
// vitest.config.ts with coverage thresholds
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: ['node_modules/**', 'src/test/**', '**/*.config.*'],
      thresholds: {
        lines: 80,
        branches: 70,
        functions: 80,
        // Per-file thresholds for critical modules
        perFile: true,
      },
      // Force CI to fail if thresholds aren't met
      checkCoverage: true,
    },
  },
});

// Run coverage:
// npx vitest --coverage

// Coverage output:
// File          | % Stmts | % Branch | % Funcs | % Lines
// --------------|---------|----------|---------|--------
// src/auth      |   95.2  |   88.9   |   100   |   95.2
// src/payments  |   91.4  |   85.3   |   95.0  |   91.4
// src/utils     |   100   |   100    |   100   |   100
Try it yourself — TYPESCRIPT