White Box · Structure-Based

Branch Coverage

Exercise every branch from every decision point — both true and false. Branch coverage is stronger than statement coverage and is the standard minimum for most production code.

Senior Test Lead ISTQB CTFL v4.0 — 4.3.2

1 The Hook

An Auckland council builds an online rates-rebate calculator. A homeowner on a low income should get a rebate; everyone else pays full rates. The developer codes the check, the tester runs one case — a low-income applicant — sees the rebate apply, and signs it off. Statement coverage hits 100%.

Months later, an audit finds the council has been quietly handing rebates to people who do not qualify. The else branch — the path for applicants who are not eligible — was wired wrong, and it was never once tested. The single low-income test only ever took the true side of the decision. The false side ran for the first time in production, on real ratepayers.

That is the gap branch coverage closes. A decision has two ways out, and a test that only ever goes one way leaves the other completely unverified. Branch coverage forces you to take both exits of every decision — the true and the false — so the path no test has walked cannot hide a defect.

2 The Rule

For 100% branch coverage, every decision point must be taken both ways — the true outcome and the false outcome each exercised at least once. Branch coverage subsumes statement coverage, and is the standard minimum for production business logic.

3 The Analogy

Analogy

Testing both directions of every turnstile at the ferry terminal.

At the Wellington ferry terminal, a turnstile is a decision: a valid Snapper card lets you through, an invalid one stops you. Statement coverage is checking that the turnstile has been used. Branch coverage is making sure you have tested it with a valid card (it opens) and with an invalid card (it blocks). If you only ever test with a valid card, you have no idea whether the gate actually stops a fare-dodger — the "false" path has never been tried.

Every if in your code is a turnstile. Branch coverage says: walk through it once with a card that works and once with a card that does not, for every gate in the building.

What it is

Branch coverage (also called decision coverage) measures whether every branch from every decision point in the code has been taken at least once. A decision point is any point where execution can split: if/else, switch, while, for, ternary operators.

For every decision, there are at least two branches: the path taken when the condition is true, and the path taken when it’s false. 100% branch coverage requires both to be tested.

Worked example

Branch coverage on a discount function
function getDiscount(user, total) {
  let discount = 0;
  if (user.isMember) {          // Decision 1: TRUE branch / FALSE branch
    discount = 0.10;
    if (total > 100) {          // Decision 2: TRUE branch / FALSE branch
      discount = 0.15;
    }
  }
  return discount;
}
Minimum test cases for 100% branch coverage
TestInputD1 branchD2 branch
TC1Non-member, any totalFALSE ✓— (not reached)
TC2Member, total ≤ 100TRUE ✓FALSE ✓
TC3Member, total > 100TRUE (already covered)TRUE ✓

Three tests achieve 100% branch coverage — and in doing so, they also achieve 100% statement coverage. Branch coverage always subsumes statement coverage.

vs statement coverage

100% statement coverage is achievable with a single test (member, total = 150). That test never triggers the non-member path or the below-$100 path. Branch coverage forces you to test those paths too.

Rule of thumb: target 100% branch coverage for business logic, utility functions, and validation code. Accept lower thresholds for generated code, UI templates, and error handlers that can’t be practically triggered.

Coverage targets by context

  • Safety-critical / financial code: 100% branch coverage, plus MC/DC (Modified Condition/Decision Coverage)
  • Core business logic: 100% branch coverage
  • Standard application code: 80%+ branch coverage is a common industry target
  • Generated or boilerplate code: exclude from measurement

ISTQB mapping

ISTQB CTFL v4.0 reference
RefTopic
4.3.2Branch Testing and Coverage
FL-4.3.2 K2Explain branch testing and branch coverage
FL-4.3.2 K2Explain the value of branch coverage over statement coverage

4 Now You Try

Three graded exercises — spot, fix, then build. Write your answer, run it for AI feedback, then compare to the model answer.

🔍 Exercise 1 of 3 — Spot: which branch is missed

A NZ public-transport concession function has two decisions. A tester runs two cases: (a) isStudent=true, fare=4 and (b) isStudent=true, fare=10. Identify which branches of D1 and D2 are taken, and name the branch that is never tested.

function concession(isStudent, fare) {
  let price = fare;
  if (isStudent) {            // D1
    price = fare * 0.75;
    if (fare > 8) {           // D2
      price = price - 1;
    }
  }
  return price;
}
Show model answer
Test (a) isStudent=true, fare=4: D1=TRUE, D2=FALSE (4 is not > 8).
Test (b) isStudent=true, fare=10: D1=TRUE, D2=TRUE.

Branch never tested: D1=FALSE. Both tests use isStudent=true, so the non-student path (the else/false side of D1) is never taken. The whole block that skips the discount has not run.

Why a third test is needed: branch coverage requires BOTH sides of every decision. A third test with isStudent=false (any fare) takes D1=FALSE and completes branch coverage. Without it, a bug in the full-price path for non-students would go undetected.
🔧 Exercise 2 of 3 — Fix: repair a redundant suite

A tester claims the suite below gives 100% branch coverage for the concession function above. It does not, and it contains a redundant test. Rewrite it as the minimum set that achieves 100% branch coverage.

Flawed suite:
TC1: isStudent=true, fare=10
TC2: isStudent=true, fare=12
TC3: isStudent=true, fare=4

Rewrite as minimum 100% branch-coverage suite:

Show model answer
Minimum 100% branch-coverage suite (3 tests):
- TC1: isStudent=false, fare=any → D1=FALSE
- TC2: isStudent=true, fare=4   → D1=TRUE, D2=FALSE
- TC3: isStudent=true, fare=10  → D1=TRUE, D2=TRUE

What was wrong with the original:
- Missing branch: every original test had isStudent=true, so D1=FALSE was never covered. The suite was NOT 100% branch coverage despite the claim.
- Redundant test: TC1 (fare=10) and TC2 (fare=12) both give D1=TRUE, D2=TRUE — identical branch outcomes. One of them adds no new branch coverage.
- The fix swaps a redundant true/true test for a D1=FALSE test, reaching all four branch outcomes in three tests.
🏗️ Exercise 3 of 3 — Build: minimum suite for a new function

An IRD refund function has the decisions below. Design the minimum set of test cases for 100% branch coverage, giving inputs and the branch outcome of each decision for every test.

function refund(overpaid, hasBankAccount) {
  let action = 'hold';
  if (overpaid > 0) {            // D1
    if (hasBankAccount) {        // D2
      action = 'pay';
    } else {
      action = 'cheque';
    }
  }
  return action;
}
Show model answer
Minimum 100% branch coverage: 3 tests.
- TC1: overpaid=0, hasBankAccount=any → D1=FALSE, D2 not reached → result 'hold'
- TC2: overpaid=50, hasBankAccount=true → D1=TRUE, D2=TRUE → result 'pay'
- TC3: overpaid=50, hasBankAccount=false → D1=TRUE, D2=FALSE → result 'cheque'

Three tests cover all four branch outcomes: D1 true and false, D2 true and false. D2 only needs covering when D1 is true (otherwise it is never reached), so TC2 and TC3 both keep D1=TRUE and flip D2. A senior would note that TC2 at exactly overpaid=1 would also serve as a BVA boundary test, but that is a separate concern from branch coverage.

Self-Check

Click each question to reveal the answer.

Q1: What does 100% branch coverage require that 100% statement coverage does not?

Branch coverage requires both the true and the false outcome of every decision to be exercised. Statement coverage only requires each line to run once, which can happen while a decision is taken one way only. Branch coverage forces the untaken side of every if, loop, and ternary to be tested.

Q2: Why does branch coverage subsume statement coverage?

If every branch from every decision is taken, then execution has reached every reachable statement along the way — so 100% branch coverage automatically gives 100% statement coverage. The reverse does not hold: 100% statement coverage can leave a whole branch untaken.

Q3: A function has two nested decisions, D1 then D2 (D2 only runs when D1 is true). What is the minimum number of tests for 100% branch coverage, and why?

Three. One test takes D1=false (D2 is never reached); one takes D1=true with D2=true; one takes D1=true with D2=false. You cannot exercise D2 without D1 being true, so both D2 outcomes share the D1=true path, giving three tests in total.

Q4: When is a lower branch-coverage threshold than 100% reasonable?

For generated code, UI templates, and error handlers that cannot be practically triggered, an 80%+ target is a common industry compromise. The full 100% is reserved for business logic, validation, and utility functions; safety-critical or financial code goes further still, adding MC/DC.

Q5: Does 100% branch coverage prove a compound condition like A && B is fully tested?

No. Branch coverage only checks the overall decision outcome, true and false. A compound condition can reach both overall outcomes without each atomic condition (A and B) being evaluated both ways. Catching that needs condition coverage or MC/DC, which sit above branch coverage.

Try It — Design for branch coverage

A NZ loyalty rewards function has two decisions (D1, D2). Select the minimum set of test cases needed to achieve 100% branch coverage — both TRUE and FALSE branches of every decision.

Function: calculateReward(user, purchaseAmount)
function calculateReward(user, purchaseAmount) {
  let points = 0;
  if (user.isLoyaltyMember) {          // D1
    points = purchaseAmount * 2;
    if (purchaseAmount >= 100) {       // D2
      points = points * 1.5;            // bonus multiplier
    }
  }
  return points;
}

Select the test cases to include in your branch coverage suite: