Loading content...
When a coverage tool reports "85% coverage," what exactly does that 85% represent? The answer depends on the type of coverage being measured. Different coverage types analyze different aspects of code execution, each revealing distinct insights about test completeness.
Understanding coverage types is like understanding different medical tests. A blood pressure reading, cholesterol panel, and cardiac stress test all measure "heart health," but they examine it from different angles and catch different problems. Similarly, line coverage, branch coverage, and path coverage all measure "test completeness" but detect different gaps in your test suite.
This page explores the three most important coverage types, building from the simplest to the most comprehensive—and most demanding.
By the end of this page, you will understand line coverage, statement coverage, branch coverage, and path coverage. You'll know what each type measures, how they relate to one another, their respective strengths and limitations, and how to choose the right coverage type for different testing contexts.
Line coverage (also called statement coverage) is the simplest and most commonly reported coverage metric. It measures the percentage of executable lines (or statements) in your code that were executed during testing.
The calculation:
Line Coverage = (Lines Executed / Total Executable Lines) × 100%
Note that "executable lines" excludes blank lines, comments, import statements, and declarations. Only lines that produce runtime behavior count.
123456789101112131415161718
function calculateShipping(weight: number, destination: string): number { let baseCost = 5.00; // Line 2: Executable if (weight > 10) { // Line 4: Executable baseCost += (weight - 10) * 0.50; // Line 5: Executable } if (destination === 'international') { // Line 8: Executable baseCost *= 2.5; // Line 9: Executable } return Math.max(baseCost, 3.00); // Line 12: Executable} // Test: calculateShipping(5, 'domestic')// Executes: Lines 2, 4, 8, 12// Misses: Lines 5, 9// Line Coverage: 4/6 = 66.7%What line coverage reveals:
What line coverage misses:
Line coverage has a critical blind spot: it doesn't distinguish how a line was reached. Consider a line inside a conditional:
if (a || b) {
doSomething(); // This line is "covered" if either a OR b is true
}
If your test only triggers this line when a is true, you get 100% line coverage for this block—but you've never tested the scenario where a is false and b is true. The behavior might differ between these cases, and line coverage won't catch the gap.
Line coverage serves as a good starting point—if a line is never executed, it's definitely not tested. But high line coverage can mask significant gaps. A test suite with 90% line coverage might still miss critical edge cases hidden in conditional logic.
| Aspect | Description |
|---|---|
| Measures | Percentage of executable lines that ran during tests |
| Granularity | Coarse—whole lines, regardless of internal logic |
| Ease of Achievement | Relatively easy to get high percentages |
| Common Threshold | 70-90% for most production code |
| Best For | Quick health check, identifying obviously untested code |
| Limitation | Ignores conditional complexity within lines |
Branch coverage (also called decision coverage) goes deeper than line coverage. It measures whether every possible branch from each decision point has been executed. For every if, else, switch case, ternary operator, and loop condition, branch coverage asks: Have we tested both the "taken" and "not taken" paths?
The calculation:
Branch Coverage = (Branches Executed / Total Branches) × 100%
A simple if statement creates two branches (true and false). A switch with five cases creates five branches. A for loop creates two branches (condition true to continue, condition false to exit).
123456789101112131415161718192021222324252627282930313233
function getDiscountTier( purchaseCount: number, memberYears: number): string { // Decision 1: Two branches (true/false) if (purchaseCount >= 100) { // Decision 2: Two branches (true/false) if (memberYears >= 5) { return 'platinum'; // Branch 1A, 2A } else { return 'gold'; // Branch 1A, 2B } } // Decision 3: Two branches (true/false) if (purchaseCount >= 50) { return 'silver'; // Branch 1B, 3A } return 'bronze'; // Branch 1B, 3B} // Total branches: 6 (2 from Decision 1, 2 from Decision 2, 2 from Decision 3) // Test 1: getDiscountTier(150, 10) → 'platinum'// Covers: Decision 1(true), Decision 2(true)// Test 2: getDiscountTier(25, 1) → 'bronze'// Covers: Decision 1(false), Decision 3(false)// Branch Coverage: 4/6 = 66.7% // Missing: Decision 2(false), Decision 3(true)// Need: getDiscountTier(150, 2) → 'gold'// getDiscountTier(75, 1) → 'silver'Why branch coverage matters:
Branch coverage catches edge cases that line coverage misses. Consider error handling:
function fetchUser(id: string): User {
const response = await api.get(`/users/${id}`);
if (!response.ok) {
throw new UserNotFoundError(id);
}
return response.data;
}
With line coverage, you might achieve 100% by only testing successful fetches—the throw line counts as "covered" even if it never actually throws. Branch coverage requires you to test the !response.ok case explicitly.
For most production code, branch coverage provides the best balance of insight vs. effort. It catches significantly more issues than line coverage without the exponential test explosion of path coverage. Industry standard targets are often 70-80% branch coverage.
Condition coverage examines the individual boolean sub-expressions within a decision. While branch coverage treats if (a && b) as a single decision with two outcomes, condition coverage drills into the components: Has a been both true and false? Has b been both true and false?
The calculation:
Condition Coverage = (Boolean Conditions Evaluated to True + False / Total Possible Evaluations) × 100%
For a condition with two sub-expressions, there are 4 possible evaluations (each can be true or false), but condition coverage only requires that each sub-expression is true at least once and false at least once.
1234567891011121314151617181920212223242526272829303132
function isEligibleForLoan( creditScore: number, income: number, hasCollateral: boolean): boolean { // Three conditions: A && B && C // A: creditScore >= 700 // B: income >= 50000 // C: hasCollateral if (creditScore >= 700 && income >= 50000 && hasCollateral) { return true; } return false;} // BRANCH COVERAGE requires:// - At least one test where entire condition is TRUE// - At least one test where entire condition is FALSE// Minimum: 2 tests // CONDITION COVERAGE requires each sub-condition evaluated both ways:// - creditScore >= 700: tested with ≥700 AND <700// - income >= 50000: tested with ≥50000 AND <50000// - hasCollateral: tested with true AND false // Test 1: isEligibleForLoan(750, 60000, true) → true// A=true, B=true, C=true// Test 2: isEligibleForLoan(650, 40000, false) → false// A=false, B=<not evaluated due to short-circuit>, C=<not evaluated> // Problem: Short-circuit evaluation means B and C might not be tested!The short-circuit problem:
In most programming languages, compound boolean expressions use short-circuit evaluation:
A && B: If A is false, B is never evaluatedA || B: If A is true, B is never evaluatedThis means achieving true condition coverage requires careful test design to ensure every sub-expression actually executes with both true and false values—even when short-circuiting could skip them.
Modified Condition/Decision Coverage (MC/DC):
For safety-critical systems (avionics, medical devices, automotive), a stricter variant called MC/DC is often required. MC/DC demands that each condition independently affects the decision outcome. This means proving that flipping any single condition—while holding others constant—changes the final result.
MC/DC is mathematically rigorous but requires significantly more tests.
Condition coverage becomes important when compound boolean expressions encode complex business rules. If your eligibility logic combines multiple criteria with AND/OR, testing each criterion's influence independently catches bugs that branch coverage misses.
Path coverage is the most comprehensive (and demanding) coverage type. It measures whether every possible execution path through the code has been tested. A path is a unique sequence of branches taken from function entry to function exit.
The calculation:
Path Coverage = (Unique Paths Executed / Total Possible Paths) × 100%
The exponential problem:
Paths explode combinatorially. Consider a function with 10 sequential if statements:
if creates 2 possible branchesAdd a loop, and paths become potentially infinite (the loop could execute 0, 1, 2, ... n times). In practice, full path coverage is often infeasible for non-trivial code.
1234567891011121314151617181920212223242526272829303132333435
function processOrder(order: Order): ProcessResult { let total = order.subtotal; // Decision 1: 2 paths if (order.hasCoupon) { total -= applyCoupon(order.couponCode); } // Decision 2: 2 paths → now 2 × 2 = 4 cumulative paths if (order.isMember) { total *= 0.95; // 5% member discount } // Decision 3: 2 paths → now 4 × 2 = 8 cumulative paths if (total > 100) { total -= 10; // $10 off orders over $100 } // Decision 4: 2 paths → now 8 × 2 = 16 cumulative paths if (order.isRush) { total += getRushFee(order.destination); } // Decision 5: 3 paths (express/standard/economy) → 16 × 3 = 48 paths! switch (order.shippingTier) { case 'express': total += 20; break; case 'standard': total += 10; break; case 'economy': total += 5; break; } return { total, status: 'processed' };} // This simple function has 48 unique execution paths!// 100% path coverage requires 48 distinct test cases.When path coverage is valuable:
Despite its impracticality for full coverage, path coverage thinking is valuable for:
Practical path coverage strategies:
Rather than pursuing 100% path coverage (often impossible), experienced engineers:
| Coverage Type | What It Measures | Test Count Growth | Typical Target |
|---|---|---|---|
| Line/Statement | Lines executed | Linear | 80-90% |
| Branch/Decision | Decision outcomes taken | Linear to # of branches | 70-85% |
| Condition | Boolean sub-expressions | Linear to # of conditions | 70-80% |
| Path | Complete execution traces | Exponential (2^n) | Selective, <100% |
100% path coverage would theoretically test every possible execution scenario. But in practice, it's computationally intractable for most real-world code. Use path coverage thinking to identify critical paths, but rely on branch coverage for systematic coverage targets.
While line, branch, and path coverage examine code at a granular level, function coverage and class coverage operate at a higher level of abstraction. They answer simpler questions: Was this function ever called? Was this class ever instantiated?
Function coverage:
Function Coverage = (Functions Called / Total Functions) × 100%
Function coverage is coarse—it doesn't care what happens inside the function, only whether the function was invoked. This is useful for quickly identifying dead code or forgotten utilities.
Why function coverage alone is insufficient:
A function like calculateTax(income, deductions, filingStatus) might have dozens of internal branches handling different tax brackets, deduction types, and filing scenarios. Calling it once achieves 100% function coverage while potentially exercising less than 10% of its logic.
Function coverage answers: "Is this function tested at all?" Line/branch coverage answers: "How thoroughly is this function tested?"
Both questions matter, but they reveal different insights.
1234567891011121314151617181920212223242526
class TaxCalculator { calculate(income: number, status: 'single' | 'married'): number { let tax = 0; if (status === 'married') { // Married tax brackets if (income <= 20000) tax = income * 0.10; else if (income <= 80000) tax = 2000 + (income - 20000) * 0.15; else if (income <= 160000) tax = 11000 + (income - 80000) * 0.25; else tax = 31000 + (income - 160000) * 0.30; } else { // Single tax brackets if (income <= 10000) tax = income * 0.10; else if (income <= 40000) tax = 1000 + (income - 10000) * 0.15; else if (income <= 80000) tax = 5500 + (income - 40000) * 0.25; else tax = 15500 + (income - 80000) * 0.30; } return Math.round(tax * 100) / 100; }} // Test: calculator.calculate(50000, 'single')// Function coverage: 100% (calculate was called)// Line coverage: ~25% (only one bracket for one status)// Branch coverage: ~15% (1 of 8 bracket conditions + unmarried path)Different situations call for different coverage emphases. There's no universal "best" coverage type—the right choice depends on context, risk, and resource constraints.
Decision framework:
| Situation | Recommended Coverage | Rationale |
|---|---|---|
| Quick health check | Function + Line | Fast to measure, reveals obvious gaps |
| General production code | Line + Branch | Good balance of insight and effort |
| Complex business logic | Branch + Condition | Ensures all decision combinations tested |
| Safety-critical code | MC/DC + selective Path | Regulatory requirements, maximum rigor |
| API surface validation | Function (public methods) | Ensures all public contracts have tests |
| Legacy code assessment | Line | Baseline understanding before improving |
| Performance hotspots | Path (selective) | All execution variants optimized |
Layered coverage strategy:
Sophisticated teams often apply different coverage standards to different parts of the codebase:
This tiered approach allocates testing effort proportionally to risk rather than uniformly across all code.
For most teams, branch coverage provides the best default. It catches significantly more bugs than line coverage without the exponential explosion of path coverage. Treat branch coverage as your primary metric, with line coverage as a quick sanity check.
Coverage types form a hierarchy of strength. Stronger coverage types subsume weaker ones—achieving a stronger coverage level automatically achieves the weaker levels.
The subsumption hierarchy:
Path Coverage
↓ (subsumes)
Branch Coverage / Decision Coverage
↓ (subsumes)
Statement Coverage / Line Coverage
↓ (subsumes)
Function Coverage
What subsumption means:
However, the reverse is not true:
123456789101112131415161718192021222324252627
function sign(n: number): string { if (n > 0) { return "positive"; } if (n < 0) { return "negative"; } return "zero";} // Test Set A: sign(5), sign(-3)// - Line Coverage: 100% (all lines executed)// - Branch Coverage: 100% (both branches of both ifs tested)// - Function Coverage: 100% (function called)// But wait—we never tested the "zero" case!// Actually we did: sign(5) tests n>0=true, n<0=not-reached// sign(-3) tests n>0=false, n<0=true// Where's zero? If we add sign(0):// - n>0=false, n<0=false, reaches "zero" return // Test Set B: sign(5), sign(-3), sign(0)// Now we have complete branch coverage: // - n>0: true(5), false(-3,0)// - n<0: true(-3), false(0)// Line coverage: 100%// Branch coverage: 100%// Path coverage: 100% (only 3 paths exist, all tested)Practical implication:
If you track branch coverage, you don't need to separately track line coverage—achieving branch coverage targets will naturally achieve line coverage targets (assuming reasonable test design). But tracking both can be useful for different audiences:
We've explored the landscape of coverage types, from simple line coverage to comprehensive path coverage. Let's consolidate the key insights:
What's next:
Now that we understand different coverage types, we need to confront an uncomfortable truth: even high coverage by any measure has significant limitations. The next page explores what coverage cannot tell you and why relying solely on coverage metrics leads teams astray.
You now understand the spectrum of coverage types—from simple line coverage through demanding path coverage. You know what each measures, their trade-offs, and how to select the right type for different contexts. Next, we'll explore the critical limitations of coverage metrics.