Coverage-Based Testing
Coverage is a flashlight, not a finish line. Learn how to use statement, branch, condition, and path coverage to find the gaps your unit tests are missing.
1 The Hook — Why This Matters
In 2021, a Wellington fintech startup shipped a GST calculation module that had passed all 47 unit tests with 100% statement coverage. Yet on launch day, a Christchurch exporter was charged GST on a zero-rated export sale. The bug was in a compound condition: if (isRegistered && amount > 0 && !isExport). The tests had exercised every line, but never the case where isRegistered was false while amount > 0 was true. Statement coverage was green. Branch coverage was green. Condition coverage was red.
The startup had to issue refunds, absorb the processing fees, and rebuild trust with their first enterprise client. 100% statement coverage gave them false confidence. The gap wasn't in their tests — it was in their understanding of what coverage actually measures.
2 The Rule — The One-Sentence Version
Use coverage to find untested risk, not to prove you're done testing.
Each coverage type asks a different question. Statement coverage asks "did every line run?" Branch coverage asks "did every decision go both ways?" Condition coverage asks "did every boolean sub-expression go both ways?" Path coverage asks "did every unique route through the code execute?" Higher coverage types find bugs that lower types miss — but no coverage metric finds bugs in code that was never written.
3 The Analogy — Think Of It Like...
Inspecting a building after an earthquake.
Statement coverage is walking every corridor to see if the lights turn on. Branch coverage is opening every door to check both sides. Condition coverage is testing every sprinkler head individually. Path coverage is simulating every possible evacuation route. You can walk every corridor and still miss a door that only opens inward during a fire drill. Each level reveals risks the previous one did not.
4 Watch Me Do It — Step by Step
Here is a real NZ example using a GST calculation function. Follow these steps every time you analyse coverage data.
function calculateGST(amount, isRegistered, isExport) {
if (isRegistered && amount > 0 && !isExport) {
return amount * 0.15; // line 3
}
if (!isRegistered) {
throw new Error('Not registered'); // line 6
}
return 0; // line 8
}
- Obtain the coverage report Run your test suite with a coverage tool (Istanbul, JaCoCo, Coverage.py). Export branch and condition coverage, not just statement.
- Sort by branch coverage, not statement Statement coverage is the weakest metric. A function with 100% statement coverage can still have untested branches. Sort your report by branch coverage ascending to find the riskiest gaps first.
-
Map untested branches to risk
For the GST function above, the
ifon line 2 has one decision with three conditions. Branch coverage tests the whole decision true/false. If false is never hit, export sales may be incorrectly taxed. -
Design targeted tests for uncovered branches
- Branch true:
isRegistered=true, amount=100, isExport=false→ GST = 15 - Branch false:
isRegistered=false→ throws error
- Branch true:
-
Verify condition coverage for compound decisions
Condition coverage tests each boolean sub-expression independently. For
isRegistered && amount > 0 && !isExport, you need tests where each condition is true and false while the others keep the decision reachable.isRegistered amount > 0 !isExport Decision Coverage true true true true All true false true true false isRegistered false true false true false amount false true true false false isExport false -
Measure path coverage for critical flows
Path coverage tests every unique combination. For this function there are fewer than eight paths because the second
ifdepends on the first. Trace the control flow graph and identify which paths are infeasible (dead code) versus merely untested. - Re-run and compare before/after Document the coverage delta in your pull request. Coverage should increase meaningfully, not by writing do-nothing tests.
5 When to Use It / When NOT to Use It
✅ Use coverage-based testing when...
- You can access the source code (white-box context)
- A module has high business or financial risk
- You are reviewing developer unit tests for gaps
- A bug keeps escaping despite black-box testing
- You need to justify where to add regression tests
- You work in regulated industries (finance, health, aviation)
❌ Don't use it when...
- You don't have source code access (pure black-box)
- You treat coverage percentage as a management target
- The cost of instrumentation outweighs the value (legacy UI)
- You chase 100% path coverage on non-critical code
- You use it to replace thinking ("80% means we're fine")
- You ignore exception paths and error handling branches
6 Common Mistakes — Don't Do This
🚫 Gaming coverage with do-nothing tests
I used to think: If I write a test that calls every function without assertions, coverage goes up and my job is done.
Actually: Coverage without assertions is theatre. A test that calls calculateGST() but doesn't check the return value gives you 100% statement coverage and 0% confidence. Coverage is a map to untested areas, not a score to optimise.
🚫 Ignoring short-circuit evaluation
I used to think: If branch coverage is green, the boolean logic is fully tested.
Actually: In A && B && C, if A is false, B and C are never evaluated. Branch coverage of the decision true/false does not guarantee each sub-condition was tested independently. You need condition coverage or MC/DC for that.
🚫 Chasing 100% path coverage everywhere
I used to think: More coverage is always better, so path coverage is the ultimate goal.
Actually: Path coverage grows exponentially. A function with ten sequential if statements has over 1,000 paths. Path coverage is valuable for critical algorithms (payment calculations, tax logic) but economically infeasible for UI glue code. Match the coverage type to the risk level.
7 Now You Try — Interview Warm-Up
Question: Consider this function. How many tests are needed for 100% branch coverage? How many for 100% condition coverage?
if (age >= 18 && hasConsent) {
process();
} else {
reject();
}
Write your answer before revealing.
Answer:
Branch coverage: 2 tests. One where the decision is true (both conditions true) and one where it is false (at least one condition false).
Condition coverage: Minimum 3 tests. You need age >= 18 as both true and false, and hasConsent as both true and false, while keeping the decision reachable each time. For example: (18, true) → true; (17, true) → false; (18, false) → false. This covers both conditions independently.
8 Self-Check — Can You Actually Do This?
Click each question to reveal the answer. If you got all three, you're ready to apply this on the job.
Q1. What's the difference between branch coverage and condition coverage?
Branch coverage tests each decision (the whole if condition) as true and false at least once. Condition coverage tests each boolean sub-expression independently — every && and || operand must be true and false. A single branch test can miss a condition-level bug if short-circuit evaluation hides it.
Q2. Why is 100% statement coverage dangerous?
Because it only proves every line executed once. It says nothing about whether decisions were tested both ways, whether compound conditions were evaluated independently, or whether your assertions actually validate behaviour. A suite with 100% statement coverage and weak assertions gives false confidence.
Q3. What is MC/DC and when should you use it?
Modified Condition/Decision Coverage (MC/DC) requires that every condition independently affects the decision outcome. It is stronger than condition coverage and is mandated for safety-critical software (aviation, medical devices). Use it when a single boolean error could cause catastrophic failure.
9 Interview Prep — Common Questions
Q. "What's the difference between branch and condition coverage?"
Branch coverage tests each decision true and false. Condition coverage tests each boolean sub-expression independently. In A && B, branch coverage can be satisfied with two tests: (true, true) and (false, *). But condition coverage requires A to be true and false, and B to be true and false, which needs at least three tests because short-circuit evaluation prevents B from being evaluated when A is false.
Q. "A developer says they have 100% unit test coverage. Should you still do exploratory testing?"
Absolutely. Coverage measures what code ran, not what behaviour was validated. It doesn't catch missing requirements, integration failures, race conditions, or usability issues. I would review their coverage report to see if it's statement or branch, check for assertions, and then test the integration boundaries and edge cases that unit tests typically miss.
Q. "How do you decide which coverage metric to target?"
I match the metric to the risk. For low-risk UI code, statement coverage is enough. For business logic like tax calculations, I target branch plus condition coverage. For safety-critical code, I require MC/DC. I also pair coverage with mutation testing to verify that the tests would fail if the logic changed.
Q. "What do you do when path coverage is infeasible?"
I identify infeasible paths through the control flow graph — paths that can never execute due to logical dependencies between conditions. I document them as excluded paths, target the feasible paths with highest risk, and use branch or condition coverage as the practical metric for the remaining code.