Test Coverage & Quality Metrics
Measure and improve test quality - code coverage vs. mutation testing, quality gates, test debt tracking, and building a testing culture.
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
- 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%.
- 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.
- 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.
- 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.
- 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 doneExamples
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/