Test Coverage Guide
Overview
Section titled “Overview”Test coverage measures which parts of your code execute during testing. Think of it as a heat map showing which code paths your tests exercise versus which remain untested.
Understanding Coverage Types
Section titled “Understanding Coverage Types”Line Coverage
Section titled “Line Coverage”Line coverage tracks whether each executable line runs during tests. Imagine highlighting lines in your code with a marker as they execute—line coverage represents the percentage of highlightable lines you marked.
Branch Coverage
Section titled “Branch Coverage”Branch coverage ensures both paths of conditional logic get tested. Picture a railroad switch—branch coverage verifies your tests travel down both tracks:
function greet(user) { if (user.isPremium) { // This creates a branch return `Welcome back, ${user.name}!`; // Path 1 } else { return `Hello, ${user.name}`; // Path 2 }}With only one test for a premium user, you achieve 100% line coverage (every line runs) but only 50% branch coverage (the non-premium path remains untested).
Statement Coverage
Section titled “Statement Coverage”Similar to line coverage but counts individual statements rather than lines. Multiple statements on one line count separately.
Function Coverage
Section titled “Function Coverage”Tracks whether each function gets called during testing. Useful for identifying completely untested functions.
How to Measure Coverage
Section titled “How to Measure Coverage”Generate coverage during test execution:
# Run tests with coverage collectiondeno test --coverage=cov_profile
# Generate HTML reportdeno coverage cov_profile --html
# Generate lcov report for CI toolsdeno coverage cov_profile --lcov > coverage.lcov
# View coverage in terminaldeno coverage cov_profileNode.js with Jest
Section titled “Node.js with Jest”Configure Jest in package.json or jest.config.js:
{ "scripts": { "test": "jest", "test:coverage": "jest --coverage" }, "jest": { "collectCoverageFrom": [ "src/**/*.{js,jsx}", "!src/index.js" ], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } }}Run coverage:
npm run test:coverage
# Generate HTML reportjest --coverage --coverageReporters=html
# Open coverage reportopen coverage/index.htmlNode.js with Vitest
Section titled “Node.js with Vitest”Configure in vitest.config.js:
import { defineConfig } from "vitest/config";
export default defineConfig({ test: { coverage: { provider: "v8", // or 'istanbul' reporter: ["text", "json", "html"], exclude: [ "node_modules/", "test/", ], }, },});Run coverage:
# Run tests with coveragevitest run --coverage
# Watch mode with coveragevitest --coveragePython with pytest
Section titled “Python with pytest”Install coverage tools:
pip install pytest-covRun with coverage:
# Basic coverage reportpytest --cov=myproject tests/
# Generate HTML reportpytest --cov=myproject --cov-report=html tests/
# Set minimum coverage thresholdpytest --cov=myproject --cov-fail-under=80 tests/Built-in coverage support:
# Run tests with coveragego test -coverprofile=coverage.out ./...
# View coverage reportgo tool cover -html=coverage.out
# Get coverage percentagego test -cover ./...
# Detailed function-level coveragego tool cover -func=coverage.outJava with JaCoCo (Maven)
Section titled “Java with JaCoCo (Maven)”Add to pom.xml:
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.10</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions></plugin>Run coverage:
mvn clean testmvn jacoco:report# Report available at target/site/jacoco/index.htmlCoverage Goals and Strategy
Section titled “Coverage Goals and Strategy”The Pragmatic Approach
Section titled “The Pragmatic Approach”Recommended targets:
- Critical business logic: 85-95%
- API endpoints: 80-90%
- Data models: 80-90%
- Utility functions: 70-80%
- UI components: 60-70%
- Configuration files: Skip or minimal
Benefits of High Coverage (80-90%)
Section titled “Benefits of High Coverage (80-90%)”- Early regression detection: Catches breaking changes immediately
- Enforced edge case thinking: Forces consideration of error paths
- Refactoring confidence: Safe code modifications
- Living documentation: Tests demonstrate expected behavior
Drawbacks of 100% Coverage
Section titled “Drawbacks of 100% Coverage”- Diminishing returns: Final 10-20% often covers trivial code
- Test brittleness: Over-specified tests break with minor changes
- Maintenance burden: More tests mean more upkeep
- False security: High coverage doesn’t guarantee quality tests
What to Exclude from Coverage
Section titled “What to Exclude from Coverage”Consider excluding:
- Generated code
- Third-party vendored code
- Simple getters/setters
- Framework boilerplate
- Debug utilities
- Migration scripts
Example exclusion in various tools:
// Istanbul (JS)/* istanbul ignore next */if (process.env.NODE_ENV === "debug") { console.log(state);}# Pythondef debug_only(): # pragma: no cover print("Debug information")Best Practices
Section titled “Best Practices”Focus on Quality Over Quantity
Section titled “Focus on Quality Over Quantity”Write tests that verify behavior, not implementation:
// ❌ Poor: Tests implementation detailstest("uses array push method", () => { const spy = jest.spyOn(Array.prototype, "push"); addItem(list, item); expect(spy).toHaveBeenCalled();});
// ✅ Good: Tests behaviortest("adds item to list", () => { const list = ["apple"]; addItem(list, "banana"); expect(list).toContain("banana");});Prioritize Branch Coverage
Section titled “Prioritize Branch Coverage”Branch coverage often reveals more bugs than line coverage. Complex conditionals hide edge cases:
// This function needs 4 tests for full branch coveragefunction processPayment(amount, user) { if (amount > 0 && user.hasValidCard) { // Test 1: amount > 0 AND hasValidCard = true return chargeCard(amount); } else if (amount > 0 && !user.hasValidCard) { // Test 2: amount > 0 AND hasValidCard = false return requestCardUpdate(); } else if (amount <= 0 && user.hasValidCard) { // Test 3: amount <= 0 AND hasValidCard = true return refundCard(Math.abs(amount)); } else { // Test 4: amount <= 0 AND hasValidCard = false return handleError(); }}Use Coverage Trends
Section titled “Use Coverage Trends”Track coverage over time rather than absolute numbers. A dropping trend signals technical debt accumulation.
Integrate with CI/CD
Section titled “Integrate with CI/CD”Fail builds when coverage drops below thresholds:
# GitHub Actions example- name: Run tests run: npm test -- --coverage
- name: Upload coverage uses: codecov/codecov-action@v3 with: fail_ci_if_error: true minimum_coverage: 80Conclusion
Section titled “Conclusion”Test coverage serves as a useful metric but not an absolute goal. Like a map showing unexplored territory, it guides you toward untested code without demanding you explore every inch. Aim for comprehensive coverage of critical paths while accepting that perfect coverage often means imperfect resource allocation.
The real goal: confidence that your tests catch meaningful bugs before users do.