Engineering4 min read

Test Coverage & Quality Metrics

Measure and improve test quality - code coverage vs. mutation testing, quality gates, test debt tracking, and building a testing culture.

Test CoverageCode QualityMutation TestingSonarQubeQuality Gates

skill: Test Coverage & Quality Metrics category: Engineering tags: [Test Coverage, Code Quality, Mutation Testing, SonarQube, Quality Gates] readingTime: 4

Code coverage is a floor, not a ceiling. 80% coverage with poor tests is worse than 60% coverage with excellent tests - the false sense of security is more dangerous than acknowledged gaps. Mutation testing measures test quality, not just quantity. The best metric is team confidence: do engineers deploy on Friday without anxiety?

Workflow

  1. Understand what coverage measuresCoverage metrics (line, branch, statement) tell you what code was executed, not whether it was meaningfully tested. A test that exercises code without asserting anything raises coverage to 100%.
  2. Use mutation testing for test qualityMutation testing modifies your source code (mutants) and checks whether your tests catch the change. If a mutant survives (tests still pass), you have a test gap.
  3. Set up quality gates with SonarQubeQuality gates prevent code with unacceptable quality metrics from being merged. SonarQube analyzes code for bugs, vulnerabilities, code smells, and coverage - blocking PRs that regress quality.
  4. Track and reduce test debtTest debt accumulates in the same way as technical debt. Areas without tests grow riskier over time. Regular measurement and targeted investment reduces risk systematically.
  5. Build a testing cultureMetrics without culture revert. Tests are added under pressure only to be deleted when release pressure hits. A testing culture makes testing the natural path of least resistance.

Output Format

Test quality: vitest coverage with thresholds (lines/branches/functions), Stryker mutation score (>70% target), SonarQube quality gate on new code, risk-score = (100-coverage) × complexity for debt prioritization, and DoD with test requirements.

vitest coverage: thresholds { lines:80, branches:75, functions:80 }
Stryker: thresholds { high:80, low:60, break:50 } - mutation score
SonarQube: coverage >= 80% + Rating A (no bugs/vulnerabilities) as gate
Risk = (100 - lineCoverage) × cyclomaticComplexity → prioritize tests
DoD: "tests written + coverage threshold + reviewed" before story done

Examples

Input

Output

Edge Cases & Gotchas

  • Generated code coverage - Exclude auto-generated code (protobuf, GraphQL types, ORM migrations) from coverage reports - they inflate coverage without providing test value. Add to sonar.exclusions and vitest coverage.exclude.
  • Mutation testing timeout - Mutation testing runs each mutant against the test suite - 1000 mutants × 30s test suite = 8+ hours. Use perTest coverage analysis (Stryker option) to only run tests affected by each mutation, reducing runtime 10-50×.
  • Coverage in monorepos - Monorepo coverage combines all packages. Track coverage per-package, not just global - a high-coverage utility package can mask an untested business logic package.
  • Branch coverage for optional chaining - Optional chaining (?.) and nullish coalescing (??) create implicit branches in Istanbul/V8 coverage. 100% line coverage may leave these branches uncovered. Branch coverage is more valuable for these patterns.

When to Break These Rules

  • Skip formal coverage thresholds for generated code, thin adapters, and configuration files - enforce thresholds on domain logic and business rules where bugs matter most.
  • Don't require 100% coverage for legacy codebases being incrementally tested - ratcheting (coverage must not decrease) is more productive than targeting an absolute number for brownfield projects.

Reference Files

  • https://stryker-mutator.io/
  • https://docs.sonarsource.com/sonarqube/

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.