Code Coverage
Code coverage measures how much of your code is executed during tests. It's a useful metric for identifying untested code, but it's important to understand what it can and cannot tell you.
Running Coverage Reports
Generate a coverage report with:
npm test -- --coverage
Or add a script to package.json:
{
"scripts": {
"test:coverage": "jest --coverage"
}
}
Understanding Coverage Metrics
Jest reports four types of coverage:
1. Statement Coverage
Percentage of statements executed:
function example(x) {
const a = 1; // Statement 1
const b = 2; // Statement 2
if (x > 0) {
return a + b; // Statement 3
}
return 0; // Statement 4
}
// Test: example(1)
// Covers statements 1, 2, 3
// Statement coverage: 75% (3/4)
2. Branch Coverage
Percentage of decision branches taken:
function example(x, y) {
if (x > 0) { // Branch 1: true/false
if (y > 0) { // Branch 2: true/false
return 'both';
}
return 'x only';
}
return 'neither';
}
// Test: example(1, 1) and example(-1, 1)
// Branches covered: x>0 true, x>0 false, y>0 true
// Branches NOT covered: y>0 false
// Branch coverage: 75% (3/4)
3. Function Coverage
Percentage of functions called:
function add(a, b) { return a + b; } // Function 1
function subtract(a, b) { return a - b; } // Function 2
function multiply(a, b) { return a * b; } // Function 3
// Test: add(1, 2), subtract(3, 1)
// Function coverage: 66% (2/3)
4. Line Coverage
Percentage of executable lines run:
function process(items) {
const results = []; // Line 1
for (const item of items) { // Line 2
if (item.active) { // Line 3
results.push(item.name); // Line 4
}
}
return results; // Line 5
}
// Test: process([{ active: true, name: 'a' }])
// Lines covered: 1, 2, 3, 4, 5
// Line coverage: 100%
Reading Coverage Reports
Terminal Output
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
----------|---------|----------|---------|---------|-------------------
All files | 85.71 | 75 | 66.67 | 85.71 |
math.js | 85.71 | 75 | 66.67 | 85.71 | 12-15
----------|---------|----------|---------|---------|-------------------
The "Uncovered Lines" column tells you exactly which lines need tests.
HTML Report
Jest generates a detailed HTML report in coverage/lcov-report/:
open coverage/lcov-report/index.html
This shows:
- Color-coded source files (green = covered, red = uncovered)
- Clickable files to see line-by-line coverage
- Branch indicators (E = else not covered, I = if not covered)
Configuring Coverage
In jest.config.js:
module.exports = {
collectCoverage: true,
// Which files to collect coverage from
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/**/*.stories.{js,jsx}'
],
// Coverage output directory
coverageDirectory: 'coverage',
// Coverage reporters
coverageReporters: ['text', 'lcov', 'html'],
// Minimum thresholds (fail if below)
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
// Per-file thresholds
'./src/utils/': {
branches: 100,
statements: 100
}
}
};
Coverage Thresholds
Set minimum coverage requirements:
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
Now tests fail if coverage drops below 80%:
Jest: "global" coverage threshold for branches (80%) not met: 75%
This is useful in CI to prevent merging code with insufficient tests.
What Coverage Doesn't Tell You
High coverage doesn't mean good tests!
Consider this function and test:
function isEven(n) {
return n % 2 === 0;
}
test('isEven works', () => {
isEven(2); // 100% coverage, but no assertion!
});
This test has 100% coverage but tests nothing. Coverage only measures what code runs, not whether it's correctly tested.
Coverage Limitations
- Doesn't check assertions exist - Code can run without being verified
- Doesn't test edge cases - One path through code can miss bugs
- Doesn't test behavior - Integration between units isn't measured
- Can encourage gaming - Writing tests just to hit numbers
Effective Use of Coverage
Good Practices
- Use as a guide, not a goal - Coverage reveals gaps, not quality
- Focus on critical paths - Not all code needs 100% coverage
- Review uncovered lines - Ask "should this be tested?"
- Set reasonable thresholds - 80% is often a good target
Finding Gaps
// Look at coverage report for this function
function processOrder(order) {
if (!order.items.length) {
throw new Error('Empty order'); // ← Not covered
}
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
if (item.discount) { // ← Branch not covered
total -= item.discount;
}
}
if (total > 1000) { // ← Branch not covered
total *= 0.9; // 10% bulk discount
}
return total;
}
The coverage report shows you need to test:
- Empty orders (error case)
- Items with discounts
- Orders over $1000 (bulk discount)
Try It: Identify Missing Tests
Coverage in CI/CD
Add coverage to your CI pipeline:
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
This:
- Runs tests with coverage
- Fails if thresholds aren't met
- Uploads to Codecov for tracking over time
Key Takeaways
- Coverage shows which code runs during tests
- Four metrics: statements, branches, functions, lines
- Use
--coverageflag to generate reports - HTML reports show line-by-line coverage
- Set thresholds to prevent coverage drops
- High coverage != good tests
- Use coverage to find gaps, not as the only metric
- Aim for 80%+ coverage on critical code paths
Next, we'll learn about Test-Driven Development (TDD) - writing tests before code!

