We have all seen that green “Coverage: 100%” badge. It is reassuring, it pleases managers, and it gives a sense of accomplishment.
But what does it really mean?
As a Quality Engineer, I often see confusion between “executed code” and “tested code”. Code coverage does not tell you if your code works correctly. It only tells you which lines were traversed by your tests… and more importantly, which ones were not.
Let’s dissect the four most classic columns of a report to understand where the real value lies.
Let’s take a simple function with a bit of logic: a discount calculator.
export function calculatePriceWithDiscount(price: number, isMember: boolean) {
let discount = 0; // Line 2
if (isMember) { // Line 3
if (price > 100) { // Line 4
discount = 20; // Line 5
} else { // Line 6
discount = 10; // Line 7
}
}
return price - discount; // Line 10
}discount.ts
If we run a single test: calculatePriceWithDiscount(50, false), here is the report the terminal sends back:
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 50.00 | 25.00 | 100.00 | 50.00 |
discount.ts | 50.00 | 25.00 | 100.00 | 50.00 | 4-7
-----------------|---------|----------|---------|---------|-------------------
Note on examples
The code examples we use are trivial and simplified for purely educational purposes: to show the internal mechanics of coverage.
In a professional context, real tests should include a rigorous analysis of boundaries and domains (boundary testing). This means checking edge cases, legal ceilings, floors (e.g., a price cannot be negative), and handling invalid or unexpected inputs.
Let’s analyze these columns one by one to understand why we have these scores.
1. Function coverage
Main question: “Has the function been called at least once?”
This is the most basic metric. Since our test calls calculatePriceWithDiscount, the function is marked as “seen”.
Verdict: Is my code reliable? Absolutely not. It is a “macro” metric that serves mainly to spot dead code or forgotten files.
2. Statement coverage
Main question: “Has each statement been executed?”
A “statement” is a logical unit of execution that the JavaScript engine must process. Let’s count them precisely in our code, there are exactly 6:
let discount = 0;(Initialization)if (isMember) { ... }(Main conditional structure)if (price > 100) { ... }(Nested conditional structure)discount = 20;(Assignment for case > 100)discount = 10;(Assignment for else case)return ...;(Return)
Our test calculatePriceWithDiscount(50, false) follows this path:
- It executes #1 (
let discount...). - It executes #2 (
if isMember). Since it is false, it skips the entire internal block. - It executes #6 (
return).
Summary: We executed statements 1, 2, and 6. Statements 3, 4, and 5 were never reached.
Verdict: 3 out of 6 = 50%.
3. Line coverage
Main question: “Has the text line in the file been hit?”
This is where confusion often reigns. In a well-formatted project (with Prettier), line coverage is often identical to statement coverage. But the difference is notable if your formatting is compact.
Imagine this compact line of code:
if (isMember) console.log("User is member");
This is one single line, but two statements (the if and the console.log).
If my test is isMember = false:
- The engine reads the line to evaluate the
if. -> Line coverage: 100% (The line is hit). - The engine does NOT execute the
console.log. -> Statement coverage: 50%.
In summary: Statement coverage is the machine truth. Line coverage is the human (visual) truth. If you use a strict linter that enforces “one instruction per line”, these two metrics will be synchronized.
4. Branch coverage
Main question: “Has every possible decision branch been tested?”
This is where robustness comes into play. Our code contains conditions (if), which creates branches.
Let’s count the possible branches:
if (isMember): Can be TRUE or FALSE. (2 branches)if (price > 100): Can be TRUE or FALSE. (2 branches)
Let’s visualize the flow to fully understand the 4 branches:
Mathematical total: 4 possible outcomes (the 4 arrows coming out of the diamonds).
With our test calculatePriceWithDiscount(50, false), we only took one single branch: the “No (Branch C)” arrow at the very top. We never explored the other cases.
Note: Nesting creates a logical dependency. If you test branches A (discount = 20) or B (discount = 10), you mechanically and implicitly validate passing through the isMember = true branch.
Verdict: 1 branch tested out of 4 = 25%.
The link with cyclomatic complexity
This is where branch coverage becomes a true architectural tool. This number of independent branches (here 4) corresponds to the cyclomatic complexity of your function.
Faced with high complexity, the engineer’s reflex should not be to blindly add tests, but to question the code structure:
- Reduce first (refactoring): It is always more profitable to simplify code than to complexify tests. If a function has too many branches, split it. Better two simple functions that are easy to validate than a “Rube Goldberg machine” kept alive by unreadable tests.
- Cover second: Once the logic is streamlined and stripped down to essentials, branch coverage becomes imperative to ensure that every remaining scenario is mastered.
The coverage trap
Warning. It is possible to have 100% everywhere without testing anything. Look:
test("full coverage", () => {
calculatePriceWithDiscount(150, true); // Branch A (see diagram)
calculatePriceWithDiscount(50, true); // Branch B
calculatePriceWithDiscount(50, false); // Branch C
});discount.test.ts
The metrics turn green. Everything is at 100%. Yet, I have made no assertion (expect(...)).
If tomorrow I change discount = 20 to discount = 0 (a bug), my tests will still pass, and my coverage will remain at 100%.
Conclusion
Code coverage is a valuable technical indicator for spotting dead code or blind spots, but the true safety net lies in functional coverage.
The goal is not to chase the percentage, but to ensure that every business rule and every critical use case is validated by a test. If this functional requirement is met, then your code coverage will mechanically be high and, above all, relevant.
Note: Practices such as TDD (Test Driven Development) naturally favor this approach by ensuring that every line of code produced responds to a tested functional need, de facto eliminating dead code. But that is a topic for another article.