4.5.15 · D3Software Engineering

Worked examples — Code coverage — line, branch, path coverage

2,924 words13 min readBack to topic

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

  1. Count lines. a = w*h and return a → 2 executable lines. Why this step? The denominator for line coverage is total executable lines.
  2. Run the test. area(3,4) executes both lines → line. Why? No fork means every line always runs.
  3. 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.
  4. 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

  1. Count lines: L1, D1, L2, L3 → but if age<12 and the two assignments count as statements → executable lines = price=10, if..., price=5, return price = 4. Why? Each does work at runtime.
  2. Run fee(30): age<12 is False, so price=5 (L2) never runs. Executed = 3 of 4 → line. Why this step? Shows one test typically misses the code inside an untaken if.
  3. 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.
  4. Branch for fee(30): only the False outcome taken → branch. Notice : branch is stricter.
  5. 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)

Figure — Code coverage — line, branch, path coverage
  1. Count branch outcomes: D1{T,F} + D2{T,F} = 4 outcomes. Why? Each decision independently has two.
  2. 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.
  3. 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).
  4. 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)

  1. List naive combinations: D1∈{T,F}, D2∈{T,F} → 4 on paper. Why start here? To expose the trap.
  2. 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.
  3. Enumerate feasible paths:
    • D1-T, D2-T → "big"
    • D1-T, D2-F → "small"
    • D1-F (D2 skipped) → "nonpos" → 3 feasible paths, not 4.
  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)

Figure — Code coverage — line, branch, path coverage
  1. Loop decision = one branch: i < k has True (enter loop) and False (exit) → 2 branch outcomes. Why? Every loop guard is a decision, exactly like an if.
  2. sum_to(0): 0 < 0 is 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.
  3. 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.
  4. 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.
  5. 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

  1. Decision (branch) coverage: the whole and expression 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.
  2. 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.
  3. Trace short-circuit: for can_login(None), C1 is False, and and short-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.
  4. 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

  1. Enumerate executable lines: if x>=0 (first), return "nonneg", second if x>=0, return "never", return "neg" → 5 lines.
  2. Trace both tests: sign(5) → first if True → returns "nonneg". sign(-5) → first if False → reaches second if x>=0, which is now also False (x is still -5) → skips "never" → returns "neg". Why this step? Shows return "never" can never execute for any x.
  3. 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).
  4. 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

  1. 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.
  2. Make D1 True: need verified=True and (balance>0 or premium). Test grant(True, 100, False) → verified ✓, balance>0 ✓ → True → "allow".
  3. Make D1 False: simplest is verified=False. Test grant(False, 100, True) → verified fails → whole thing False → "deny". Why this choice? If verified is False the and short-circuits to False regardless of the rest.
  4. 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.
  5. 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)

  1. 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.
  2. The catch: the test has no assert. It never checks that abs_val(-3) equals 3. Why this matters? Coverage measures "did the line run", not "did the result get checked."
  3. Inject a mutation: suppose the code said return x instead of return -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.
  4. 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.