Session 3.6 — Statement Coverage & Branch Coverage

Module 3: Dynamic Testing — White-box Techniques | Duration: 1 hour

Learning Objectives
  • Define statement coverage and compute statement coverage percentage for a given test set.
  • Identify the limitations of statement coverage that make it insufficient for high-quality testing.
  • Define branch coverage and design a minimum test set that achieves 100% branch coverage.
  • Explain why branch coverage subsumes statement coverage.
  • Use coverage measurement tools to interpret coverage reports.

Concept Overview

Statement and branch coverage are the two most fundamental white-box coverage criteria. Statement coverage ensures every line of code runs. Branch coverage goes further: it ensures every decision in the code has been exercised in both its true and false outcomes. Understanding both — and the gap between them — is essential for effective unit testing.

Statement Coverage
The weakest but most commonly reported baseline. Every executable statement in the code is executed at least once by the test suite.
Branch Coverage
Requires every decision point to be taken in both directions (true and false). Stronger than statement coverage and subsumes it.
The gap
A test set can achieve 100% statement coverage while leaving an entire else branch completely untested — branch coverage closes this gap.

Statement Coverage

Statement coverage (also called line coverage) measures the percentage of executable statements in the code that have been executed by at least one test case.

Definition and formula

An executable statement is any line of code that performs an operation when run (assignments, function calls, conditionals, return statements). Comments and blank lines are not counted.

SC% = (Number of statements executed ÷ Total number of executable statements) × 100

Achieving statement coverage
  1. List all executable statements in the code (map to CFG nodes).
  2. For each test case, trace which statements are executed.
  3. Mark statements as "covered" if executed by at least one test.
  4. Compute SC% = covered / total × 100.
  5. Add test cases to cover any remaining uncovered statements.
When SC is sufficient
For very simple, linear code with no branching. Also useful as a minimum baseline to detect completely untested modules.
Industry context
Many CI pipelines report SC as a first gate. Falling below 80% SC typically blocks a merge. However, SC alone is never the final quality bar.

Statement Coverage — Worked Example

Consider the following function that applies a discount to a cart total:

def apply_discount(total, member, coupon): S1: discount = 0 S2: if member: S3: discount += 10 S4: if coupon == "SAVE20": S5: discount += 20 S6: if total > 500: S7: discount += 50 S8: final = total - discount S9: return final

Total executable statements: 9 (S1 through S9, excluding the if conditions themselves which we count as S2, S4, S6).

Test Case TC-1: total=600, member=True, coupon="SAVE20"
StatementExecuted?Reason
S1: discount = 0YesAlways executed
S2: if memberYesAlways evaluated
S3: discount += 10Yesmember = True
S4: if coupon == "SAVE20"YesAlways evaluated
S5: discount += 20Yescoupon matches
S6: if total > 500YesAlways evaluated
S7: discount += 50Yestotal = 600 > 500
S8: final = total - discountYesAlways executed
S9: return finalYesAlways executed

SC% = 9/9 × 100 = 100% with a single test case!

Critical question: Does this mean the test suite is complete? No.

TC-1 never tests: member=False (S3 skipped but S3 exists in the false branch), coupon mismatch (S5 skipped), total ≤ 500 (S7 skipped). The false branches of S2, S4, and S6 are never taken by TC-1. This is exactly the limitation that branch coverage addresses.

Limitations of Statement Coverage

The fundamental weakness

Statement coverage does not distinguish between a statement being executed inside an if branch versus outside it. A single test that takes the true path of every condition achieves 100% SC but never tests any false path.

Bugs hiding in else branches, elif arms, and default cases of switch statements are invisible to statement-coverage-only suites.

Demonstration: a bug SC will miss
def transfer_funds(amount, balance): if amount <= balance: balance -= amount # This branch tested by TC-1 return True else: balance -= amount # BUG: should not deduct when insufficient return False

With TC-1 (amount=100, balance=500), 100% SC is achieved. The bug in the else branch (deducting when it should not) is completely missed. Only a test that triggers amount > balance (i.e., the false branch) would expose it.

Branch Coverage

Branch coverage (also called decision coverage) requires that every possible outcome of every decision point in the code is exercised at least once. For a boolean decision, this means both the true branch and the false branch must be taken by at least one test case.

Definition and formula

A branch is a single outgoing edge from a decision node in the CFG. Every decision point with N outcomes has N branches. For a binary if, there are always 2 branches: T and F.

BC% = (Number of branches executed ÷ Total number of branches) × 100

Identifying branches
  1. From the CFG, count all outgoing edges from decision nodes — these are the branches.
  2. An if with no else has 2 branches: the if body (T) and the fall-through (F).
  3. An if-else has 2 branches: the if body (T) and the else body (F).
  4. A switch with N cases has N branches (one per case, plus one for default if present).
  5. A while loop has 2 branches: enter the loop body (T) and exit the loop (F).
Branch coverage subsumes statement coverage

If both branches of every decision are exercised, then every statement reachable from an entry node must be executed. Therefore 100% BC ⇒ 100% SC. The converse is false: 100% SC does not imply 100% BC.

Branch Coverage — Worked Example

Using the same apply_discount function from earlier:

Branch identification from CFG
DecisionBranch TBranch F
D1: if memberB1: member is TrueB2: member is False
D2: if coupon == "SAVE20"B3: coupon matchesB4: coupon does not match
D3: if total > 500B5: total > 500B6: total ≤ 500

Total branches = 6 (B1 through B6)

Minimum test set for 100% Branch Coverage
TestInputsBranches CoveredSC%BC%
TC-1total=600, member=True, coupon="SAVE20"B1, B3, B5100%50%
TC-2total=400, member=False, coupon="NONE"B2, B4, B678% (S3,S5,S7 not executed)100%

TC-1 + TC-2 together: SC = 100%, BC = 100% using just 2 tests.

Trace of TC-2 (total=400, member=False, coupon="NONE")
StatementExecuted?Reason
S1: discount = 0YesAlways
S2: if memberYesEvaluated as False
S3: discount += 10Nomember = False, branch B2 taken
S4: if coupon == "SAVE20"YesEvaluated as False
S5: discount += 20Nocoupon mismatch, branch B4 taken
S6: if total > 500YesEvaluated as False
S7: discount += 50Nototal=400, branch B6 taken
S8: final = total - discountYesdiscount=0, final=400
S9: return finalYesReturns 400

Statement Coverage vs Branch Coverage

AspectStatement CoverageBranch Coverage
What is measuredEach executable statement executed at least onceEach branch outcome (T/F) of every decision taken at least once
FormulaExecuted statements / Total statementsExecuted branches / Total branches
CFG elementNodes (basic blocks)Edges from decision nodes
SubsumptionSubsumed by branch coverageSubsumes statement coverage
Minimum tests (example function)1 test can achieve 100%Typically 2+ tests needed
Bugs foundMissing/unreachable codeIncorrect branching logic, missing else handling
Industry threshold80% SC as CI gate (minimum)80–100% BC as standard quality bar
Key rule

100% branch coverage guarantees 100% statement coverage, but NOT vice versa. Always target branch coverage as the minimum acceptable criterion for unit testing.

Coverage Measurement Tools

Python: Coverage.py
Run with coverage run -m pytest then coverage report -m. Shows line and branch coverage per file. HTML report available via coverage html.
Java: JaCoCo
Integrates with Maven/Gradle. Produces XML and HTML reports showing statement, branch, and line coverage per class and method.
JavaScript: Istanbul / nyc
Used with Jest or Mocha. Reports statement, branch, function, and line coverage. Threshold enforcement via .nycrc.
C/C++: gcov / lcov
GCC built-in coverage tool. Instruments code at compile time. lcov provides HTML visualisation of line and branch coverage.
Interpreting a coverage report
Report ColumnMeaningAction if low
StmtsTotal executable statementsBaseline count
MissStatements never executedAdd tests targeting these lines
BranchTotal branch outcomesBaseline count
BrPartBranches partially covered (one direction only)Add test for missing T or F outcome
CoverCoverage percentageCompare against project threshold
MissingLine numbers not coveredGo directly to those lines and design covering tests

Common Mistakes

Stopping at 100% SC
Achieving full statement coverage with one happy-path test and calling the suite complete. Always verify branch coverage as well.
Counting if-conditions as statements
The condition itself is a decision node, not a separate statement. Count the body statements (true branch and false branch bodies) separately.
Treating 80% BC as "done"
The uncovered 20% of branches may be the most critical error-handling paths. Justify every uncovered branch explicitly.
Omitting assertions
A test that executes code but never asserts the output contributes to coverage but provides no defect detection value.

Class Activity

You are given the following function for a ride-share fare estimator:

def estimate_fare(distance, peak_hour, promo_code): base = distance * 12 if peak_hour: base *= 1.5 if promo_code == "RIDE50": base -= 50 if base < 0: base = 0 return base
  1. Label all executable statements S1, S2, …
  2. Identify all decisions and their branches (B1, B2, …).
  3. Design one test case that achieves 100% statement coverage. Compute SC%.
  4. Design a minimum test set that achieves 100% branch coverage. Compute BC% for each individual test and the combined set.
  5. Identify a bug scenario that your branch-covering test set would detect but statement-only test would miss.
Evaluation rubric (10 marks)
  • 2 marks: Correct statement labelling and count.
  • 2 marks: Correct branch identification and count.
  • 3 marks: Valid minimum test set achieving 100% BC with traced coverage.
  • 2 marks: SC% and BC% correctly computed for each test.
  • 1 mark: Valid bug scenario that SC misses but BC catches.

Exit Ticket

  1. A function has 20 executable statements and 3 binary decisions. What is the maximum number of branches? What is the minimum number of tests needed for 100% BC?
  2. True or False: If BC = 100%, then SC = 100%. Justify.
  3. A coverage report shows 90% statement coverage with two branches "partially covered". What does "partially covered" mean, and what test do you need to add?

Summary & Assignment

Statement coverage is the minimum structural baseline but leaves false branches untested. Branch coverage subsumes statement coverage and requires every decision to be exercised in both directions. Branch coverage is the recommended standard for unit testing and is enforced by modern CI/CD pipelines through tools like JaCoCo, Coverage.py, and Istanbul.

Assignment: Using your mini-project's unit test suite, (1) run a coverage tool and record the initial SC% and BC% per function. (2) Design additional test cases to bring each function to at least 90% BC. (3) Document which uncovered branches you intentionally excluded and justify why. Submit the before/after coverage report and your new test cases.