Statement coverage at 100% while decision coverage stalls
CloudLedger's billing service contains the following method. The team's exit criterion is 100% statement coverage from the coverage tool; the current build reports 100% statement coverage and 68% decision coverage. The service has been released twice with a defect in the EU regional discount path, which is only reached by non-VIP customers with amounts over 2000.
int discount(Customer c, int amount) {
int d = 0;
if (c.vip) d = 10; // line 3
if (amount > 500) d = d + 5; // line 4
if (c.vip && amount > 500) d = d + 5; // line 5
if (c.region.equals("EU") || amount > 2000) d = d + 3; // line 6
while (d > 20 && !c.blocked) d = d - 2; // line 7
return d;
}
No test currently uses an amount over 2000, and the loop body has only been exercised with a single iteration.
1. Explain how 100% statement coverage is possible with only 68% decision coverage here, using a concrete test that demonstrates the gap on line 6.
Read the worked answer
Statement coverage asks only that each executable statement has run at least once. Decision coverage asks that each decision has evaluated to both true and false. A single test therefore covers a statement while leaving one outcome of its controlling decision untested.
Concrete example for line 6: a test with c.region = "EU" and amount = 300 (non-VIP) evaluates the || condition to true, executes the increment statement, and so makes line 6 statement-covered. The false outcome - where the region is not EU and the amount is not over 2000, so d is left unchanged - has never occurred. That single test contributes 1 to statement coverage and only one of two decision outcomes.
That is exactly how the shipped defect escaped: every existing test either used an EU customer or stayed under 2000, so the branch where a non-EU customer with a high amount should have received the +3 was never observed. The same pattern explains the 68% figure: several decisions in this method have had only one outcome taken. Note also that because of short-circuit evaluation, a covered decision says nothing about the individual conditions inside it - passing the region test tells you nothing about whether amount > 2000 was ever evaluated.
2. Design the minimum set of tests to achieve MC/DC on line 6 and justify the number.
Read the worked answer
Line 6 has two atomic conditions: A = c.region.equals("EU"), B = amount > 2000. MC/DC requires that each condition be shown to independently affect the decision outcome, i.e. for each condition there is a pair of tests where that condition changes and the other condition is held fixed, and the outcome flips.
Minimum set (3 tests):
- A = true, B = false (region EU, amount 300): decision true.
- A = false, B = true (region "US", amount 2500): decision true.
- A = false, B = false (region "US", amount 300): decision false.
Independence check: tests 1 and 3 differ only in A (B false in both) and the outcome flips (true to false), so A is shown to matter. Tests 2 and 3 differ only in B (A false in both) and the outcome flips, so B is shown to matter. Both conditions are therefore covered by an independence pair, and each of tests 1 and 2 also adds an outcome not produced by the other.
Why three and not four: with N conditions, N+1 tests are the theoretical minimum under short-circuit evaluation, because the all-false test doubles as the fixed-background case for both conditions. Four tests (all combinations) is decision-condition coverage, which is stronger than required.
These tests must also be checked for the surrounding logic: verify the returned d value, not just the branch taken, because lines 3–5 also modify d, and confirm test 2 reaches the defect seen in production.
3. What should the team do about the loop on line 7 and about relying on coverage percentages as an exit criterion?
Read the worked answer
The loop guard is compound (d > 20 && !c.blocked) and short-circuiting, so it needs both condition-level and loop-level treatment.
Loop testing: identify the loop's iteration counts and exercise zero iterations, one iteration, a typical count, the maximum plausible count, and one beyond it. Here that means constructing inputs where d never exceeds 20 (no iterations), where d exceeds 20 once (one iteration), and where d reaches its attainable maximum so the loop drives d down to ≤ 20. Also test c.blocked = true with d > 20: the loop body never runs, and because the second condition is never evaluated, an ordinary decision-coverage tool may mark the guard as covered when d > 20 has only ever been true. That is a gap worth checking explicitly.
On exit criteria: coverage is a measure of what was executed, not of what was verified. The team should:
- Replace the single statement-coverage criterion with decision coverage on all new or changed code, plus MC/DC on safety- or money-critical compound conditions such as lines 6 and 7.
- Complement coverage with specification-based design (decision tables and boundary values for discount bands) so that untested requirements, not just unexecuted lines, are visible.
- Investigate why 68% was tolerated: either tests are missing or some outcomes are unreachable, and unreachable outcomes need documented justification.
- Add a coverage-delta gate in CI so coverage cannot regress on changed files.