What code coverage measures
Code coverage is a quantitative metric: it expresses the percentage of your source code that your tests actually execute. Recording coverage helps you spot source areas that your test suite never touches, which is useful when you add features or refactor existing logic.
Raising coverage numbers as your test suite grows can increase confidence that your application is being exercised. But the metric has limits, and the way you calculate it changes what the number tells you.
| File | % Statements | % Branch | % Functions | % Lines | Uncovered lines |
|---|---|---|---|---|---|
| file.js | 90% | 100% | 90% | 80% | 89,256 |
| coffee.js | 55.55% | 80% | 50% | 62.5% | 10-11, 18 |
Four ways to calculate coverage
Most coverage tooling reports four distinct metrics: function, line, branch, and statement coverage. They overlap, but each answers a different question about your tests.
To illustrate the differences, consider a small module that calculates coffee ingredients:
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
The tests targeting calcCoffeeIngredient look like this:
/* coffee.test.js */
import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown');
expect(result).to.deep.equal({});
});
});
You can run the code and tests in this live demo or inspect the repository.
Function coverage: 50%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
}
function isValidCoffee(name) {
// ...
}
Function coverage is the simplest of the four. It counts how many functions in your source were invoked by any test. The example defines two functions, calcCoffeeIngredient and isValidCoffee; only the first is called by the tests, so function coverage lands at 50%.
Line coverage: 62.5%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
Line coverage tracks the fraction of executable lines that the test suite ran. Unexecuted lines are usually places where behavior is untested. The example has eight executable lines; the tests never reach the americano condition (two lines) or the body of isValidCoffee (one line), giving 62.5%.
Declaration statements such as function isValidCoffee(name) or let espresso, water; are not executable, so line coverage ignores them.
Branch coverage: 80%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
if (coffeeName === 'espresso') {
// ...
return { espresso };
}
if (coffeeName === 'americano') {
// ...
return { espresso, water };
}
return {};
}
…
Branch coverage measures decision points: if statements, loops, and similar constructs. It checks whether tests evaluate both the true and false paths of each condition.
The example contains five branches:
- Calling
calcCoffeeIngredientwith justcoffeeName - Calling
calcCoffeeIngredientwithcoffeeNameandcup - Coffee is Espresso
- Coffee is Americano
- Other coffee
Only the Americano branch is never exercised, so branch coverage is 80%.
Statement coverage: 55.55%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
Statement coverage looks similar to line coverage, but it counts individual statements. A single line containing multiple statements counts once per line for line coverage and once per statement here.
The example has eight executable lines but nine statements. The line espresso = 30 * cup; water = 70 * cup; holds two statements. With five of nine statements executed, statement coverage is 55.55%. If you keep to one statement per line, line and statement coverage will be roughly equal.
Which metric should you start with
Tooling usually reports all four, and the one you prioritize depends on your project, testing style, and goals. Statement coverage is a sensible first target because it is easy to interpret. Branch and function coverage add information about whether conditional paths and individual functions are really being called, so they make a natural next step once statement coverage is high.
Coverage versus test coverage
These terms are often conflated, but they measure different things:
- Test coverage is qualitative: how well the test suite addresses software features and the associated risk.
- Code coverage is quantitative: how much of the code was executed during tests.
Think of an application as a house. Test coverage asks whether the tests inspect each room; code coverage asks how much of the floor the tests walked over.
Why 100% is not a guarantee
High coverage, even 100%, does not mean your code is bug-free. A test can hit every line, branch, and function without asserting anything meaningful about behavior.
For example:
/* coffee.test.js */
// ...
describe('Warning: Do not do this', () => {
it('is meaningless', () => {
calcCoffeeIngredient('espresso', 2);
calcCoffeeIngredient('americano');
calcCoffeeIngredient('unknown');
isValidCoffee('mocha');
expect(true).toBe(true); // not meaningful assertion
});
});
That suite achieves full function, line, branch, and statement coverage, but the expect(true).toBe(true) assertion always passes, no matter what the source code does. It verifies nothing.
A misleading metric is worse than none. If your suite reports 100% but the assertions are vacuous, you may believe the application is well tested. Delete or break a piece of application logic and the tests still pass.
To avoid this, review your tests for real assertions covering different scenarios, and treat coverage as one signal among several. It should not be the sole measure of test effectiveness or code quality.
Coverage across test types
Coverage works better for some test levels than others:
- Unit tests are the best fit. They deliberately exercise many small paths, which makes coverage data meaningful.
- Integration tests can contribute coverage, but with caution. They exercise a larger portion of the source, making it harder to map which test covers which line. That said, integration coverage can help with legacy systems lacking well-isolated units.
- End-to-end tests are the hardest to measure this way because they run through complex user flows. Requirement coverage is often more useful here than source-code coverage.
Coverage is one input to a testing strategy that also weighs assertion quality, application requirements, and a mix of unit, integration, end-to-end, and manual tests. The goal is not to hit a fixed percentage, but to ensure the critical logic in your code is actually exercised.



