Code Coverage — Statement, Branch, Path Coverage (2026)
In this tutorial, you'll learn about Code Coverage. We cover key concepts, practical examples, and best practices.
Code coverage is a metric that measures how much of your source code is executed during testing — reported as percentages for statements, branches, functions, and lines covered.
What You'll Learn
You'll understand statement, branch, function, line, and path coverage metrics, use Istanbul/nyc for JavaScript projects, set meaningful coverage thresholds, and learn why 100% coverage doesn't guarantee bug-free code.
Why Code Coverage Matters
Coverage metrics reveal untested code paths. If a function has 50% branch coverage, half of its decision points have never been exercised. This helps prioritize testing effort. At DodaTech, DodaZIP's compression algorithms target 95%+ branch coverage — compression edge cases can corrupt archives.
Code Coverage Learning Path
flowchart LR A[Unit Testing Guide] --> B[Code Coverage] B --> C[Mutation Testing] B --> D[Quality Gates in CI] style B fill:#f90,color:#fff
Coverage Types
| Type | What It Measures | Example |
|---|---|---|
| Statement | Each statement executed | Every line of code runs |
| Branch | Each decision outcome | Both if and else paths |
| Function | Each function called | Every method invoked |
| Line | Each line of code executed | Similar to statement |
| Path | Every possible route through code | All combinations of conditions |
Statement Coverage
Measures how many executable statements are executed.
function classifyAge(age) {
if (age >= 18) { // Statement 1
return 'adult'; // Statement 2
}
return 'minor'; // Statement 3
}
// Test only with age >= 18
test('returns adult for 18+', () => {
expect(classifyAge(20)).toBe('adult');
});
// Statement coverage: 66% (2 of 3 statements)
Branch Coverage
Measures how many decision outcomes are tested.
test('returns adult for 18+', () => {
expect(classifyAge(20)).toBe('adult');
});
test('returns minor for under 18', () => {
expect(classifyAge(15)).toBe('minor');
});
// Branch coverage: 100% (both if/else paths covered)
Path Coverage
Measures all possible paths through the code.
function discount(price, isMember, hasCoupon) {
let final = price;
if (isMember) final *= 0.9; // Path decision 1
if (hasCoupon) final *= 0.95; // Path decision 2
return final;
}
// Paths: (not member, no coupon), (member, no coupon),
// (not member, coupon), (member, coupon)
// Requires 4 tests for 100% path coverage
Using Istanbul/nyc
Istanbul is the standard JavaScript code coverage tool. nyc is its CLI.
Setup
npm install --save-dev nyc
// package.json
{
"scripts": {
"test": "nyc mocha",
"coverage": "nyc --reporter=html --reporter=text mocha"
}
}
npm run coverage
Expected output:
| ----------- | --------- | --------- | --------- | --------- | ----------- |
|---|---|---|---|---|---|
| ----------- | --------- | --------- | --------- | --------- | ----------- |
| math.js | 94.44 | 75 | 100 | 94.44 | 10-11 |
| service.js | 80 | 50 | 66.67 | 80 | 15,22-25 |
| ----------- | --------- | --------- | --------- | --------- | ----------- |
| All files | 88.23 | 66.67 | 85.71 | 88.23 |
### Configuration
```json
// .nycrc.json
{
"all": true,
"include": ["src/**/*.js"],
"exclude": ["src/**/*.test.js"],
"reporter": ["text", "html", "lcov"],
"branches": 80,
"lines": 85,
"functions": 85,
"statements": 85
}
Jest Built-in Coverage
npx jest --coverage
Configure in jest.config.js:
module.exports = {
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{js,jsx}', '!src/**/*.test.{js,jsx}'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 85,
statements: 85,
},
},
};
Setting Coverage Thresholds
| Project Type | Statement | Branch | Function |
|---|---|---|---|
| Library/Package | 95% | 90% | 95% |
| Backend API | 85% | 80% | 85% |
| Frontend SPA | 80% | 75% | 80% |
| Prototype/MVP | 60% | 50% | 60% |
Enforcing Thresholds in CI
# <a href="/devops/github-actions/">GitHub Actions</a> — enforce coverage
- run: npx jest --coverage --coverageThreshold='{"global":{"branches":80}}'
- name: Check coverage
run: |
if [ $(npx jest --coverage --silent | grep 'Branches' | awk '{print $4}' | tr -d '%') -lt 80 ]; then
echo "Branch coverage below 80%"
exit 1
fi
Why 100% Coverage Doesn't Guarantee Quality
High coverage is valuable, but it's not the whole picture.
function add(a, b) {
return a + b;
}
// 100% coverage, but zero value
test('add function works', () => {
expect(add(1, 2)).toBe(3); // What about strings? Nulls? Missing args?
});
Coverage Blind Spots
| What Coverage Misses | Example |
|---|---|
| Missing logic | Tested function but missed wrong implementation |
| Concurrency bugs | All paths covered, but race condition exists |
| Security flaws | Both branches tested, but input validation missing |
| Performance issues | Code executes, but is 100x slower than expected |
| Integration bugs | Each unit covered individually, but they don't work together |
Goodhart's Law
"When a metric becomes a target, it ceases to be a good metric." Teams optimizing for 100% coverage write tests that hit every line but verify nothing meaningful.
Best Practices
1. Focus on Branch Coverage
Branch coverage reveals untested decision paths. It's more valuable than line coverage.
2. Set Thresholds, But Don't Worship Them
Use thresholds as a floor, not a target. A PR that increases coverage by 2% is valuable even if below threshold.
3. Review Uncovered Lines
The uncovered lines report tells you exactly what's not tested. Prioritize critical paths.
npx istanbul report html
# Open coverage/index.html — red lines are uncovered
4. Don't Test Getters/Setters/Boilerplate
Trivial code that's unlikely to break doesn't need tests. Focus on logic.
5. Combine with Mutation Testing
Coverage measures what code runs. Mutation testing measures whether the tests actually verify behavior.
Common Mistakes
1. Gaming the Metric
Writing tests that execute code but assert nothing. Coverage tools don't check assertion quality.
2. Ignoring Branch Coverage
75% branch coverage means 25% of decisions are never tested. That's where bugs hide.
3. 100% Coverage as a Gate
Teams waste time testing trivial code while missing critical untested paths.
4. Coverage Without Review
Looking at the number without inspecting which lines are uncovered.
5. Not Running Coverage in CI
Coverage is only useful if it's measured consistently. Local runs differ from CI.
6. Ignoring Integration Coverage
Unit test coverage gives a false sense of safety if integration points are untested.
7. Too Low or Too High Thresholds
50% is too low to be meaningful. 100% encourages gaming. 80-90% is the sweet spot.
Practice Questions
1. What are the five types of code coverage? Statement, branch, function, line, and path coverage.
2. What is the difference between statement and branch coverage? Statement coverage measures whether each line executes. Branch coverage measures whether each decision outcome (if/else) is tested.
3. Why is 100% coverage not enough? 100% coverage means code runs, not that behavior is verified. Missing assertions, wrong logic, concurrency bugs, and integration issues aren't caught by coverage metrics.
4. What tool is used for JavaScript code coverage?
Istanbul/nyc is the standard. Jest has built-in coverage via --coverage.
5. Challenge: Improve coverage on a module without adding value. Take a module with 100% statement coverage but no meaningful assertions. Rewrite the tests to verify behavior, not just execution.
Mini Project: Coverage Report Analyzer
// coverage-analyzer.js
class CoverageAnalyzer {
constructor(report) {
this.report = report;
}
getUncoveredBranches() {
const uncovered = [];
for (const [file, data] of Object.entries(this.report)) {
if (data.branchCoverage < 100) {
data.branches.forEach((branch, index) => {
if (branch.count === 0) {
uncovered.push({
file,
branch: index,
line: branch.line,
type: branch.type,
});
}
});
}
}
return uncovered;
}
suggestPrioritization() {
const uncovered = this.getUncoveredBranches();
return uncovered
.filter(b => b.type === 'if' || b.type === 'switch')
.map(b => `Prioritize: ${b.file}:${b.line} — untested conditional`);
}
getScore() {
const files = Object.values(this.report);
const avg = (key) =>
files.reduce((s, f) => s + f[key], 0) / files.length;
return {
statements: avg('statementCoverage'),
branches: avg('branchCoverage'),
functions: avg('functionCoverage'),
lines: avg('lineCoverage'),
};
}
}
const report = {
'math.js': {
statementCoverage: 94,
branchCoverage: 75,
functionCoverage: 100,
lineCoverage: 94,
branches: [
{ line: 10, count: 5, type: 'if' },
{ line: 12, count: 0, type: 'else' },
],
},
};
const analyzer = new CoverageAnalyzer(report);
console.log(analyzer.getScore());
console.log(analyzer.suggestPrioritization());
FAQ
What's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro