Glossary
Code coverage measures the percentage of code executed by tests—a floor metric ensuring critical paths are at least validated once.
Across three companies — Shiksha Infotech, UshaOm, and Salesken — I've seen the same engineering challenges repeat. The details change but the patterns don't.
Code coverage is the percentage of your source code that is executed when your automated test suite runs. It is the most widely used metric for understanding how thoroughly your code is tested — and one of the most commonly misunderstood.
A codebase with 80% line coverage means that 80% of the lines of code were executed during testing. The remaining 20% were never touched — meaning any bugs in that code will only be discovered in production.
Code coverage is useful but insufficient on its own. High coverage with poor tests gives false confidence. Low coverage in critical paths is a ticking time bomb. The goal is not to maximize a number — it is to ensure that the code that matters most is tested well.
Not all coverage metrics are created equal. Each type measures a different dimension of test thoroughness:
What it measures: The percentage of lines of code that were executed during testing.
Example: If a function has 10 lines and tests execute 7 of them, line coverage is 70%.
Limitation: A line can be executed without being meaningfully tested. A test that calls a function but never checks the return value achieves line coverage without catching bugs.
def calculate_discount(price, is_member):
if is_member:
return price * 0.8 # Line covered if test passes is_member=True
else:
return price # Line NOT covered if no test with is_member=False
If you only test with is_member=True, you get 80% line coverage but miss the non-member path entirely.
What it measures: The percentage of decision branches (if/else, switch cases, ternary operators) that were taken during testing.
Why it matters more than line coverage: Branch coverage catches the gaps that line coverage misses. A function might have 100% line coverage but 50% branch coverage if tests never exercise the else path.
Example:
def validate_age(age):
if age < 0: # Branch 1: age < 0
return "invalid"
elif age < 18: # Branch 2: 0 <= age < 18
return "minor"
else: # Branch 3: age >= 18
return "adult"
Full branch coverage requires at least three tests: one for negative age, one for under 18, and one for 18+.
What it measures: The percentage of functions or methods that were called at least once during testing.
Use case: Quickly identifies completely untested modules. If function coverage is 60%, you know 40% of your functions have zero tests.
What it measures: The percentage of all possible execution paths through the code that were tested.
Why it is the most thorough: Path coverage tests every combination of decisions, not just individual branches. For a function with 3 independent if-statements, branch coverage requires 6 tests (2 per branch). Path coverage requires 8 tests (2^3 combinations).
Limitation: Path coverage grows exponentially with complexity. For most codebases, 100% path coverage is impractical.
Based on industry standards and practical experience:
| Coverage Level | Percentage | Meaning |
|---|---|---|
| Excellent | 80%+ | Most code paths tested. High confidence in changes. |
| Good | 60-80% | Critical paths covered. Some gaps in edge cases. |
| Acceptable | 40-60% | Major features tested. Significant untested code. |
| Risky | 20-40% | Many untested paths. Changes are dangerous. |
| Critical | <20% | Essentially untested. Any change could break production. |
Important context: These benchmarks apply to line coverage, which is the most commonly reported type. Branch coverage percentages are typically 10-20% lower than line coverage for the same codebase.
Not all code needs the same coverage:
| Code Area | Target Coverage | Rationale |
|---|---|---|
| Business logic | 85%+ | Revenue-critical. Bugs here cost money. |
| API endpoints | 80%+ | External-facing. Bugs affect users directly. |
| Data processing | 80%+ | Data corruption is hard to reverse. |
| Authentication/authorization | 90%+ | Security-critical. Must test all paths. |
| UI components | 60-70% | Visual bugs are lower severity. Snapshot tests help. |
| Utility functions | 70%+ | Widely used. Bugs propagate. |
| Configuration/glue code | 30-50% | Low complexity. Integration tests cover most paths. |
# Jest (built-in coverage)
npx jest --coverage
# Vitest
npx vitest --coverage
# Istanbul/nyc (any test runner)
npx nyc mocha
# pytest with coverage plugin
pytest --cov=src --cov-report=html
# coverage.py directly
coverage run -m pytest
coverage report
coverage html
# Built into Go toolchain
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
<!-- JaCoCo in Maven -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
Most teams track coverage in CI to prevent regression:
# GitHub Actions example
- name: Run tests with coverage
run: npx jest --coverage --coverageReporters=json-summary
- name: Check coverage threshold
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$COVERAGE < 70" | bc -l) )); then
echo "Coverage $COVERAGE% is below 70% threshold"
exit 1
fi
Code coverage is one dimension of overall code health. A module can have 90% coverage but still be unhealthy if:
Conversely, a module can have 60% coverage and be healthy if:
The most useful way to think about coverage is as a risk indicator. Low coverage in code that changes frequently is high risk. Low coverage in stable code that never changes is low risk.
The most impactful coverage strategy is to prioritize coverage for code that changes often:
| Change Frequency | Current Coverage | Priority |
|---|---|---|
| Changes weekly | <50% | Critical — fix immediately |
| Changes weekly | 50-80% | High — improve this quarter |
| Changes monthly | <50% | Medium — plan for improvement |
| Changes monthly | 50-80% | Low — acceptable for now |
| Rarely changes | Any | Very low — don't invest here |
Chasing 100% coverage. The last 10-20% of coverage is the most expensive to achieve and the least valuable. It typically covers error handling edge cases, platform-specific branches, and generated code. The ROI drops dramatically after 80%.
Counting coverage without checking assertion quality. A test that calls a function but never checks the result achieves coverage without testing anything. This is "assertion-free testing" and it creates false confidence.
# BAD: Achieves coverage but tests nothing
def test_calculate_discount():
calculate_discount(100, True) # No assertion!
# GOOD: Actually tests the behavior
def test_calculate_discount():
assert calculate_discount(100, True) == 80.0
Measuring global coverage instead of per-module. A global 75% can hide the fact that your billing module (critical) has 30% coverage while your static pages (non-critical) have 95%.
Adding tests only for new code. This leaves your legacy code — often the most complex and bug-prone — permanently untested. When legacy code changes (and it will), bugs slip through.
Using coverage as a developer performance metric. Coverage measures codebase quality, not individual performance. Using it to evaluate developers incentivizes gaming the number.
Use coverage reports to find untested code in high-risk areas. Prioritize:
Adopt the rule: every PR that changes code must maintain or improve coverage for the changed files. This incrementally improves coverage without requiring a massive testing sprint.
AI tools can generate test scaffolding that achieves coverage quickly. Review the generated tests for meaningful assertions, but use them as a starting point.
If code is hard to test, it is usually poorly structured. Use dependency injection, separate business logic from infrastructure, and break large functions into smaller ones.
A codebase where coverage increases 1% per month is healthier than one where coverage is static at 70%. Focus on the direction, not the absolute number.
Q: What is a good code coverage target? A: 70-80% line coverage for most projects. Focus on 85%+ for critical business logic and security-related code. Do not chase 100% — the ROI drops sharply after 80%.
Q: Does high code coverage mean no bugs? A: No. Code coverage measures what code is executed during tests, not whether the tests are correct. A test with no assertions achieves coverage without catching bugs. High coverage with good assertions is what prevents bugs.
Q: How do you increase code coverage? A: Start by requiring coverage checks on all new PRs. Then identify critical untested code using coverage reports and write targeted tests. Focus on high-risk, frequently-changing code first. Use AI tools to generate test scaffolding.
Q: What is the difference between line coverage and branch coverage? A: Line coverage measures whether each line of code was executed. Branch coverage measures whether each decision path (if/else, switch cases) was taken. Branch coverage is more thorough because it catches untested conditional paths that line coverage can miss.
Keep reading
Related resources