Worked examples — Code coverage — line, branch, path coverage
Before anything, three plain-word reminders (we earn every term):
The scenario matrix
Every worked example below is tagged with the cell of this matrix it exercises. Together they hit every cell.
| Cell | Code shape | What makes it tricky |
|---|---|---|
| A | Straight line, no if |
Degenerate: 0 branches — what does branch coverage even mean? |
| B | Single if / no else |
The "hidden" fall-through False outcome |
| C | Two sequential ifs |
Path explosion ; branch cheap, path dear |
| D | Nested if |
Some paths are infeasible — the denominator shrinks |
| E | Loop (runs times) | Infinite/huge path count; loop-boundary branches |
| F | Short-circuit and/or |
Sub-conditions hide inside one decision |
| G | Dead / unreachable code | 100% branch still leaves lines at 0% — impossible to reach |
| H | Word problem (real login) | Translate business rules into coverage targets |
| I | Exam twist | "100% line but a live bug" — the vanity-metric trap |
Example 1 — Cell A: no decisions at all
- Count lines.
a = w*handreturn a→ 2 executable lines. Why this step? The denominator for line coverage is total executable lines. - Run the test.
area(3,4)executes both lines → line. Why? No fork means every line always runs. - Count branch outcomes. There is no
if→ 0 branches. Why this matters? Branch coverage is . By convention tools report this as 100% (nothing to miss) — never as 0% or an error. - Count paths. With no forks there is exactly one route → our test covers it → path.
Verify: Line . Branch: defined as . Path . Sanity: with zero decisions all three levels must agree — the hierarchy Line ⊂ Branch ⊂ Path only separates them when decisions exist.
Example 2 — Cell B: single if, no else
- Count lines: L1, D1, L2, L3 → but
if age<12and the two assignments count as statements → executable lines =price=10,if...,price=5,return price= 4. Why? Each does work at runtime. - Run
fee(30):age<12is False, soprice=5(L2) never runs. Executed = 3 of 4 → line. Why this step? Shows one test typically misses the code inside an untakenif. - Count branch outcomes: D1 has True and False → 2 outcomes. Why? Even without an
else, "skip the block" is the False outcome — it is invisible in the source but real. - Branch for
fee(30): only the False outcome taken → branch. Notice : branch is stricter. - Reach 100%: add
fee(5)(True outcome). Both outcomes hit → branch, and now L2 runs too → line.
Verify: With
fee(30)alone: line , branch . With both tests: line , branch . This confirms the parent's theorem: 100% branch ⟹ 100% line.
Example 3 — Cell C: two sequential ifs (path explosion)

- Count branch outcomes: D1{T,F} + D2{T,F} = 4 outcomes. Why? Each decision independently has two.
- Check the two tests hit all 4:
(T,T)takes D1-True and D2-True;(F,F)takes D1-False and D2-False. All four outcomes appear → branch. Why this step? Branch only asks each outcome to happen once, not in every combination. - Count paths: with independent decisions, total routes : TT, TF, FT, FF. Look at the figure — four colored routes, our two tests use only the diagonal (TT and FF).
- Path coverage: covered → path. Why this matters? A bug living only in the
(T,F)combination is invisible to 100% branch but caught by path coverage.
Verify: Branch . Path . Total paths . Confirms the exponential blow-up: 3 sequential
ifs would give paths.
Example 4 — Cell D: nested if (infeasible paths shrink the denominator)
- List naive combinations: D1∈{T,F}, D2∈{T,F} → 4 on paper. Why start here? To expose the trap.
- Prune infeasible ones: D2 sits inside D1's True block. When D1 is False, D2 is never reached — so
(D1=F, D2=T)and(D1=F, D2=F)are the same single route "nonpos". Why this step? Path coverage counts feasible paths only; unreachable combinations are removed from the denominator. - Enumerate feasible paths:
- D1-T, D2-T → "big"
- D1-T, D2-F → "small"
- D1-F (D2 skipped) → "nonpos" → 3 feasible paths, not 4.
- Design tests:
band(150)→ big;band(50)→ small;band(-1)→ nonpos. Three tests → path.
Verify: Feasible paths (not ). Tests needed for 100% path . Branch outcomes (D1 T/F, D2 T/F) and the same 3 tests take D1-T (twice), D1-F, D2-T, D2-F → branch. So here 100% path is reached with only 3 tests because nesting removed a path.
Example 5 — Cell E: a loop (huge/infinite path count)

- Loop decision = one branch:
i < khas True (enter loop) and False (exit) → 2 branch outcomes. Why? Every loop guard is a decision, exactly like anif. sum_to(0):0 < 0is False immediately → takes the False (exit) outcome, loop body never runs. Why include this test? It's the zero-iteration degenerate case — the boundary that catches "off-by-one" bugs.sum_to(3): enters the loop (True outcome) three times, then exits (False). So this one test alone takes both outcomes → branch. Why? A loop naturally exercises both guard outcomes once it runs at least once.- Count paths: a path is defined by how many times the loop ran: 0, 1, 2, 3, … iterations — one distinct path each. Since is unbounded, total paths are effectively infinite (see figure: each loop-count is its own route). Full path coverage is infeasible.
- Practical rule: for loops, cover the 0, 1, and "many" iteration cases (boundary testing) instead of all paths.
Verify: Branch outcomes ;
sum_to(3)alone gives branch . Also check the code is correct: and . Feasible paths grow with → unbounded, so path coverage infeasible.
Example 6 — Cell F: short-circuit hides a sub-condition
- Decision (branch) coverage: the whole
andexpression is either True or False.can_login(None)→ whole condition False → False outcome.can_login(active_user)→ both parts True → whole True. → both decision outcomes hit → decision. Why? Decision coverage only looks at the combined result.
- Now look at each sub-condition. Condition coverage needs C1 (
user is not None) and C2 (user.active) each to be both True and False. Why this step? Bugs can hide in one sub-term. - Trace short-circuit: for
can_login(None), C1 is False, andandshort-circuits — Python never evaluates C2. So C2 is never False in our tests. C2 got: True (in the active-user test) but never False. Why it matters? We only exercised 3 of the 4 needed (C1-T, C1-F, C2-T) → C2-F missing. - Condition coverage: . To fix, add
can_login(inactive_user)(C1 True, C2 False) → condition. This is exactly what MC/DC enforces for safety-critical code.
Verify: Decision . Condition with two tests . After the third test . This proves 100% decision can coexist with only 75% condition — the two levels disagree.
Example 7 — Cell G: dead / unreachable code
- Enumerate executable lines:
if x>=0(first),return "nonneg", secondif x>=0,return "never",return "neg"→ 5 lines. - Trace both tests:
sign(5)→ firstifTrue → returns "nonneg".sign(-5)→ firstifFalse → reaches secondif x>=0, which is now also False (x is still -5) → skips "never" → returns "neg". Why this step? Showsreturn "never"can never execute for any x. - The unreachable line: line 4 (
return "never") has 0 reachable inputs. So max lines executable = 4 of 5. Why it matters? Coverage tools cap you below 100% not because your tests are weak, but because the code has a defect (dead code). - Best possible line coverage: . The remaining 20% signals "delete this dead code." Why? A coverage number stuck below 100% is a smell pointing at unreachable logic.
Verify: Reachable lines , total → ceiling . Confirms dead code makes 100% line coverage impossible regardless of test effort.
Example 8 — Cell H: real-world word problem
- Identify the single decision D1. For branch coverage we need D1 True once and False once → 2 outcomes. Why? Branch counts the whole condition's result.
- Make D1 True: need
verified=Trueand (balance>0orpremium). Testgrant(True, 100, False)→ verified ✓, balance>0 ✓ → True → "allow". - Make D1 False: simplest is
verified=False. Testgrant(False, 100, True)→ verified fails → whole thing False → "deny". Why this choice? Ifverifiedis False theandshort-circuits to False regardless of the rest. - Branch result: two tests give both D1 outcomes → branch with 2 tests. Why note this? A real feature's coverage target reduces to counting decision outcomes.
- Bonus — condition depth: conditions are C1=
verified, C2=balance>0, C3=premium. Full condition coverage needs each both T and F → up to 4–5 tests. For a payment system, use MC/DC.
Verify: Minimum tests for 100% branch . Decision outcomes . Check the logic table:
grant(True,100,False)="allow"(T·(T∨F)=T),grant(False,100,True)="deny"(F·anything=F). Both correct.
Example 9 — Cell I: exam twist (100% line, live bug)
- Coverage math:
abs_val(-3)takes the True outcome (return -x),abs_val(3)takes False (return x). Both branch outcomes and all lines executed → branch, line. Why? Coverage counts execution, and both tests executed everything. - The catch: the test has no
assert. It never checks thatabs_val(-3)equals3. Why this matters? Coverage measures "did the line run", not "did the result get checked." - Inject a mutation: suppose the code said
return xinstead ofreturn -x(a mutant). The test still runs, still gives 100% coverage, and still passes because it asserts nothing. The bug survives. Why show this? This is exactly what Mutation Testing detects — a surviving mutant with 100% coverage. - The fix: add real assertions:
assert abs_val(-3) == 3. Now the mutant would fail. Coverage is a lower bound on ignorance, not a proof of correctness.
Verify: Line , branch for the two-call test. Yet the correct outputs are and ; an unasserting test cannot distinguish correct code from the mutant
return x(which gives ). Confirms 100% coverage ≠ correctness.
Recall Self-check across all cells
Straight-line code branch coverage is defined as ::: 100% (0/0 by convention — no decisions to miss).
Two independent sequential ifs: branch % vs path % with the diagonal tests (TT, FF) ::: 100% branch (4/4) but 50% path (2/4).
Nested if: why 3 feasible paths not 4? ::: The inner decision is unreachable when the outer is False, so those combinations collapse into one path.
A while loop's path count ::: One path per iteration-count (0,1,2,…) → effectively infinite → full path coverage infeasible.
Short-circuit a and b with a=False: which condition never gets a False value tested? ::: b — it is never evaluated, so its outcomes may stay untested (condition < 100%).
Dead code's effect on maximum line coverage ::: It caps line coverage below 100% no matter how many tests you write.
100% line + no asserts proves what about correctness? ::: Nothing — coverage measures execution, not verification; pair with Mutation Testing.
Related deep dives: Unit Testing · Control Flow Graph · Cyclomatic Complexity · Test-Driven Development · MC-DC Coverage.