20 min read · 9 self-checks · Updated June 2026

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.

💬
Senior Engineer Insight

The nastiest branch-coverage trap I keep seeing is compound conditions — if (isEligible && hasBankAccount). Most coverage tools count this as two branches: true and false on the whole expression. Teams hit 80%, ship, and never realise they only tested cases where both sub-conditions agreed. Then production finds the edge case where one is true and the other is false. I watched this burn a Wellington benefits system: the eligibility check passed, the bank-account check failed silently, the else branch ran — but it had never been tested with that particular mix. On any Revenue NZ, Benefits NZ, or KiwiSaver logic with compound decisions, branch coverage alone is not enough. You need to explicitly design test cases that flip each sub-condition independently, or move straight to MC/DC. Branch coverage is the floor, not the ceiling.

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 Industry Reality

🏭 What you actually encounter on the job
  • Coverage tooling reports a percentage, not quality — teams hit 80% branch coverage on a codebase where the remaining 20% contains the error-handling paths that fail in production. The number becomes a compliance checkbox rather than a safety net.
  • Legacy codebases at NZ banks and government agencies commonly have zero coverage instrumentation. When you join and ask about branch coverage targets, you are often told the target is "what we can get without breaking the build." Baseline first, then improve incrementally.
  • Nested switch statements in ERP integrations (payroll, Revenue NZ GST returns) can generate hundreds of branches from generated code. Experienced testers exclude generated/boilerplate paths from measurement rather than inflating the suite with meaningless tests.
  • Developers and testers frequently disagree about what counts as a "branch." A ternary operator, a short-circuit &&, and a null-coalescing ?? all create branches in the execution graph that most coverage tools track separately. Agree on tooling before quoting numbers to management.
  • Time pressure routinely means branch coverage is applied selectively. Senior testers negotiate: 100% on the payment and rebate logic, 80% on the rest, zero on the auto-generated Swagger client. That conversation is part of the job — not a failure of process.

5 When to Use It — and When Not To

⚡ Decision guide

✓ Use it when

  • The code contains business logic with meaningful true/false decisions — eligibility checks, pricing rules, Revenue NZ tax thresholds, benefit calculations.
  • A missed branch in production would cause a financial error, data loss, or compliance breach (CCCFA lending rules, Privacy Act access controls).
  • You are reviewing a pull request and need to verify the new decision paths are actually tested, not just that lines are hit.
  • The team already has statement coverage and wants a systematic step up in confidence without jumping straight to MC/DC.
  • You are preparing for ISTQB certification or writing a test strategy that needs a named coverage criterion with a measurable target.

✗ Skip it when

  • The code is auto-generated (ORM migrations, Swagger clients, GraphQL type scaffolding) — instrumenting it adds noise and inflates your suite with meaningless tests.
  • The decision point is in a UI template or view layer where the branching is visual layout, not logic — exploratory or visual-regression testing is a better fit.
  • You are testing a third-party library or vendor API integration — you do not own the branches, and you cannot change them.
  • The risk is low enough that a happy-path smoke test is proportionate — not every CRUD endpoint needs full branch analysis.
  • The compound conditions in the decision are complex enough to hide interaction faults — at that point, move up to condition coverage or MC/DC rather than treating branch coverage as sufficient.

Context guide

How the right level of branch coverage effort changes based on project context.

Context Priority Why
Revenue NZ tax-calculation rules (e.g. PAYE thresholds, KiwiSaver contribution logic) Essential Compound conditions with independent legislative significance — a missed false branch can silently miscalculate tax for thousands of earners. Escalate to MC/DC for compound conditions.
Benefits NZ benefit eligibility portal (residency, income, age checks) Essential Untested false branches on a government distribution system are a compliance risk under the Social Security Act 2018. Every decision path that can deny or grant a benefit must be exercised.
Harbour Bank / Pacific Bank lending engine (CCCFA affordability decisions) Essential The Credit Contracts and Consumer Finance Act 2003 requires lenders to make responsible lending decisions. Untested decline-path branches expose the bank to regulatory action and customer harm.
TeleNZ / Pacific Air e-commerce checkout (pricing, promo codes, loyalty redemption) High Pricing and discount branches have direct revenue impact. 100% branch coverage on core pricing logic; 80%+ on surrounding UI and session management is a reasonable split.
TransitNZ (TransitNZ) road-licensing eligibility workflow High Incorrect licence approvals or denials carry safety and legal implications. Target 100% branch coverage on eligibility decisions; generated boilerplate from the licensing platform can be excluded.
Internal CMS or content-preview toggles (layout branching, dark-mode flags) Low Branches control visual layout, not business logic — a missed branch produces a cosmetic glitch, not a financial or compliance defect. Visual-regression testing is a better fit here.

Trade-offs

What you gain and what you give up when you choose branch coverage.

Advantage Disadvantage Use instead when…
Catches untested decision paths that statement coverage misses entirely — the most common source of production bugs on logic-heavy code. Does not verify that each atomic sub-condition in a compound expression is independently tested — if (isResident && income > 48000) still passes with two tests that never flip both conditions independently. Use MC/DC when Revenue NZ or Benefits NZ logic has compound conditions where each factor carries independent legal significance and interactions between sub-conditions could produce distinct faults.
Subsumes statement coverage — achieving 100% branch coverage automatically guarantees 100% statement coverage, so you get both metrics from one suite. A high coverage percentage provides false confidence if the tests assert nothing meaningful — a suite that executes every branch but checks no return values will still show green. Pair with mutation testing (e.g. Stryker, PITest) when your team needs to verify that branch-covering tests would actually detect real defects — not just execute the code.
Tooling is mature and widely available — Istanbul/nyc, JaCoCo, coverage.py all report branch coverage natively and integrate into CI pipelines with a single configuration line. Applying it to auto-generated code (ORM migrations, Swagger clients) inflates the test suite with meaningless tests and distorts the coverage headline — exclusions must be configured deliberately. Use exploratory testing or visual-regression testing when branching is in the UI/template layer and a missed branch produces a cosmetic difference rather than a logic defect.
Minimum-test efficiency — a well-designed suite achieves 100% branch coverage in far fewer test cases than naive line-by-line testing, keeping the suite fast and focused. Does not cover the full set of execution paths through the function — two tests can cover all branches while missing a dangerous combination of branch outcomes that only occurs on one specific path. Use path coverage when a function's branch-outcome combinations interact in ways that individual branch tests cannot reveal — typically complex workflow or state-machine logic.

Enterprise reality

How branch coverage changes at 200-300-developer scale in NZ

  • Coverage measurement is automated at pipeline level, not per-developer. At KiwiFirst Bank, branch coverage thresholds are enforced as hard CI gates — a pull request that drops coverage below the tier-specific minimum (100% on payment logic, 80% on integration adapters) is blocked before it reaches code review. The QA team's job shifts from running coverage tools by hand to owning the tier policy and adjudicating exceptions.
  • Governance and compliance require documented branch-coverage rationale, not just a number. Under the Privacy Act 2020 and NZISM, organisations processing personal data must demonstrate adequate testing of access-control logic. Auditors ask which branches of your data-access decisions were tested and what evidence exists — a screenshot of a CI dashboard is not sufficient; you need a traceable link from requirement to test to branch outcome in your test management tool.
  • Tooling at volume means a unified coverage platform, not developer-chosen tools per squad. Ten squads using Istanbul, JaCoCo, coverage.py, and dotCover independently produce numbers that cannot be rolled up into a coherent portfolio view. Mature NZ enterprises standardise on a single coverage aggregation layer — SonarQube is the most common — so that architecture and QA leadership can analyse branch-coverage trends across the entire codebase, not just per service.
  • Cross-squad coordination on shared libraries is where branch-coverage gaps most often hide. When a lending-eligibility library is owned by Squad A but consumed by Squads B through J, each squad's unit tests cover only their own integration paths — no single squad takes responsibility for the full branch map of the shared component. At this scale, branch-coverage ownership must be explicitly assigned at the library level; otherwise the uncovered 15% sits exactly in the shared code that every squad depends on.

What I would do

Professional judgement — when to reach for branch coverage, when to skip it, and what to watch for.

If…
I am joining a team that tests the CoverNZ online claims portal, and they tell me they have 82% branch coverage but cannot tell me which branches are uncovered
I would…
Pull up the coverage report in the CI tool before doing anything else, and map every red branch to the business rule it represents. On a government compensation system, an untested false branch on an eligibility or fraud-check decision is not a metric problem — it is a risk the organisation has unknowingly accepted. I would present the uncovered branches to the test lead with a plain-English description of what each one does, and use that conversation to negotiate which gaps get filled before the next release.
If…
I am reviewing a pull request for a Pacific Bank home-loan affordability function that introduces a compound condition — if (dti < 0.35 && isNZResident) — and the PR includes two tests that both use a NZ-resident applicant
I would…
Block the PR and request two additional tests: one with a non-resident applicant whose DTI is under the threshold, and one with a resident whose DTI is over. The existing tests only cover the overall decision outcome — they never independently flip the isNZResident sub-condition, which means a bug introduced specifically in that path would pass the suite unnoticed. On CCCFA-regulated lending code, I would note this in the review comment and link the requirement to escalate compound conditions to condition coverage or MC/DC.
If…
I am estimating test effort for a TeleNZ billing-engine rewrite that includes both hand-written pricing logic and a large volume of auto-generated Swagger client code, and the project manager asks for 90% branch coverage across the board
I would…
Push back on the uniform 90% target and propose a tiered policy instead: 100% branch coverage on pricing, discounting, and tax logic; 80%+ on application integration code; generated Swagger client excluded from measurement entirely via Istanbul's ignore directive. A blanket 90% applied across generated code forces the team to write hundreds of tests for branches that were never written by a human and cannot contain a logic defect — wasting sprint capacity and diluting the signal. I would document the tiered policy in the test strategy so the threshold is auditable, not just an informal agreement.

The bottom line: A coverage percentage without a branch map is just a number. Know which branches you have not tested, own the risk decision explicitly, and never let a CI gate substitute for understanding what your untested code actually does.

6 Best Practices

✓ What experienced testers do
  • Map decisions before writing tests. Draw or list every decision point in the function under test, number them (D1, D2 …), and track which branches each test case covers. This prevents the common mistake of writing five tests that all take the same branch paths.
  • Use the minimum number of tests, not the maximum. Redundant tests slow the suite and mask the coverage picture. Start with the minimum set that covers all branches, then add tests only to cover additional requirements or boundary values.
  • Combine with boundary value analysis on the same test cases. The boundary of a branch condition (e.g. total >= 100 at exactly 100) is both a branch-coverage concern and a BVA concern. One test case can serve both purposes.
  • Verify coverage with tooling, not eyeballing. Istanbul/nyc for JavaScript, JaCoCo for Java, coverage.py for Python. Run the tool and look at the highlighted uncovered branches — do not trust a manual trace for non-trivial functions.
  • Treat an uncovered branch as a defect risk, not a metric failure. When a branch is red in the coverage report, ask: "What happens if this path runs in production?" If the answer is "I don't know," that is the defect you need to find.
  • Exclude generated and boilerplate code from coverage targets explicitly. Configure your coverage tool's exclusion list (Istanbul's /* istanbul ignore next */, JaCoCo's @ExcludeFromCodeCoverage) so the headline number is meaningful.
  • Review branch coverage during code review, not just at the end. When a developer adds a new if, check immediately whether the test suite has cases for both branches. Catching it at review is far cheaper than retrofitting tests later.
  • Document your coverage thresholds in the test strategy. "80% branch coverage on application code, 100% on payment and eligibility logic, generated code excluded" is a defensible, auditable position. Undocumented targets cannot be enforced.
  • Know when to escalate to MC/DC. If a decision condition is compound (age > 65 && isResident), branch coverage does not verify each atomic condition independently. Flag this during planning and agree whether the risk warrants MC/DC.

7 Common Misconceptions

❌ Myth: 100% branch coverage means the code is fully tested.

Reality: Branch coverage only verifies that each branch was executed at least once. It says nothing about the correctness of the output, the handling of combined conditions, or what happens at boundary values. A test that takes the true branch of an eligibility check but uses an incorrect expected result contributes to branch coverage while missing the bug entirely. Coverage is a necessary but not sufficient condition for confidence.

❌ Myth: If I have 100% statement coverage, I probably have 100% branch coverage too.

Reality: Statement coverage is strictly weaker. A single test that always follows the true path of every if can achieve 100% statement coverage while leaving every false branch completely unexercised. The Auckland rates-rebate example at the top of this page is a real pattern: one happy-path test, full statement coverage, broken else branch shipping to production. Branch coverage exists specifically to close this gap.

❌ Myth: You need one test per branch, so a function with 10 branches needs 10 tests.

Reality: One test case can exercise multiple branches simultaneously. In a nested decision structure (D1 then D2), a single test that takes D1=true and D2=true covers two branches at once. The minimum number of tests for full branch coverage is determined by the structure of the decision graph, not by counting branches. For many real functions, three to four carefully chosen tests cover all branches — far fewer than the branch count.

8 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 Revenue NZ 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.

Why teams fail here

  • Treating a coverage percentage as a pass/fail gate without inspecting which branches are uncovered — the uncovered 15% is often the error-handling code most likely to fail in production.
  • Writing multiple tests that all take the same branch paths — hitting 80% branch coverage with ten tests that never once flip D1 to false.
  • Conflating branch coverage with test quality — a test that takes every branch but asserts nothing meaningful still shows green in the coverage report while missing every bug.
  • Applying branch coverage to generated or vendor code and then wondering why the suite is slow and the numbers are misleading — configure exclusions before quoting a coverage figure to management.

Key takeaway

Branch coverage doesn't tell you your code is correct — it tells you which paths you've never dared to test, and those are exactly the paths that will surprise you in production.

How this has changed

The field moved. Here is how Branch Coverage evolved from its origins to current practice.

1970s

Branch coverage (decision coverage) defined alongside statement coverage in early structural testing literature. McCabe's cyclomatic complexity (1976) provides a theoretical basis for branch analysis. Testing is academic and manual.

1990s

Code coverage tools begin appearing commercially. LDRA, Bullseye, and gcov provide branch coverage measurement for C/C++. Regulatory standards (DO-178B for avionics, IEC 61508 for safety-critical) mandate 100% branch coverage for the highest integrity levels.

2000s

JaCoCo, Istanbul, and language-specific coverage tools make branch measurement accessible to every development team. Coverage reports become standard CI artefacts — often required by project managers without a clear pass/fail criterion.

2010s

Research establishes that 100% branch coverage does not guarantee bug-free code and that mutation testing reveals coverage that does not actually test behaviour. Teams begin pairing branch coverage with mutation scores for a more meaningful quality signal.

Now

AI tools can generate branch-exercising tests from code analysis — automatically identifying uncovered paths. The conversation has shifted from achieving coverage thresholds to understanding what untested branches represent as risk, and whether tests actually assert on the outcomes of those branches.

Self-Check

Click each question to reveal the answer.

Interview Questions

What NZ hiring managers ask about Branch Coverage — and what strong answers look like.

Your team has 85% branch coverage but users are still reporting production bugs. What does this tell you?

Strong answer: It tells me that 85% of branches executed does not mean 85% of outcomes were asserted. Tests can execute a branch by hitting it once, but if they do not assert on the result, the coverage number is misleading. I would run mutation testing to see how many surviving mutants exist — if tests execute code but do not detect intentional bugs, the tests are not providing real protection. I would also look at which branches are in the uncovered 15%: complex conditional logic and error handling paths tend to have disproportionate bug density.

Mid/Senior

What is the difference between statement coverage and branch coverage, and why does it matter?

Strong answer: Statement coverage measures whether each line of code was executed. Branch coverage measures whether each possible outcome of every decision (if, switch, ternary, try/catch) was tested — both the true and false path. A test suite can achieve 100% statement coverage while missing entire branches if every execution takes the same path through conditionals. Branch coverage is a stricter criterion: you cannot have 100% branch coverage without also having 100% statement coverage, but you can have 100% statement coverage with poor branch coverage.

Junior/Mid

When is 100% branch coverage insufficient for a safety-critical component?

Strong answer: When each branch can be reached via many different combinations of conditions. Branch coverage requires reaching each branch at least once, but Modified Condition/Decision Coverage (MC/DC) requires that each atomic condition independently affects the decision outcome. For flight control software, a bug triggered only when three specific conditions are simultaneously true may never be found by standard branch coverage. MC/DC, required by DO-178C for the highest integrity levels, ensures every condition's influence is independently verified. For NZ medical device software, IEC 62304 may require equivalent rigour.

Senior/Lead

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.

Q6: Your team is testing Benefits NZ's benefit eligibility portal. The eligibility logic has three nested decisions: D1 checks whether the applicant is a NZ resident, D2 checks income threshold, and D3 checks age. A junior tester proposes one test case covering D1=true, D2=true, D3=true. What is wrong with this approach, and how would you design the minimum suite?

A: One test case only ever takes the true side of every decision, leaving all false branches completely untested. An applicant who fails the residency check (D1=false), one who earns above the threshold (D2=false), and one outside the age band (D3=false) each follow entirely different code paths that could contain defects. The minimum suite requires D1=false (one test), D1=true with D2=false (one test), and D1=true with D2=true with D3=false and D3=true (two more tests) — four tests in total to cover all branch outcomes. On a government system distributing public funds, untested false branches are a compliance risk, not just a quality concern.

Q7: What is the key difference between branch coverage and condition coverage, and when would you escalate from one to the other on an Revenue NZ tax-calculation rule?

A: Branch coverage exercises the overall outcome of each decision (true or false), whereas condition coverage requires every individual boolean sub-expression within a decision to be evaluated both true and false independently. For a simple if (income > 48000) they are equivalent, but for a compound rule like if (income > 48000 && isNZResident), branch coverage can pass with two tests that never independently flip each condition — you might never test a high-income non-resident or a low-income resident separately. Escalate to condition coverage or MC/DC when an Revenue NZ rule has compound conditions where each atomic factor carries independent legislative significance, or when a prior audit found that interaction faults between sub-conditions were not caught by branch tests alone.

Q8: A developer tells you: "We have 85% branch coverage on the KiwiSaver withdrawal module — that's above our 80% target, so we're good to ship." What trap might this reasoning contain, and how do you respond?

A: The trap is assuming a percentage tells you which branches are uncovered. 85% branch coverage means 15% of branches have never run — and without inspecting the coverage report, that 15% might be entirely in error-handling paths (invalid Revenue NZ numbers, failed bank transfers, overdrawn balances) that are the most likely to surface in production under real-world edge conditions. The correct response is to pull up the coverage tool, identify the uncovered branches by name, and make an explicit risk decision: are those branches low-risk boilerplate, or are they the paths that fire when a member's retirement funds fail to transfer? Coverage targets are a floor, not a pass mark — the content of the uncovered code matters as much as the number.

Q9: When is branch coverage the wrong technique to apply, even if the code is full of if statements?

A: Three scenarios where branch coverage is a poor fit: (1) Auto-generated code such as ORM migrations or Swagger client stubs — every branch was emitted by a tool, not written by a developer, so testing it adds noise without revealing logic defects. (2) UI template branching where an if controls layout rather than business logic — exploratory or visual-regression testing is more effective. (3) Vendor or third-party library internals that you do not own and cannot modify — you cannot meaningfully test branches in code you did not write and cannot change. In all three cases, configure your coverage tool to exclude those paths so the headline number reflects the code that actually matters, and redirect testing effort toward the application logic that carries real risk.

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:

Senior engineer insight

The coverage number your CI pipeline reports is almost never the number your risk conversation should be about. After ten years of shipping production code, the question I ask is not "what's our branch coverage percentage?" but "show me the red lines in the report" — because the uncovered branches are always a story about what the team assumed could never happen. On Revenue NZ integrations and KiwiSaver flows, those assumptions are where the money disappears.

The most common mistake: teams set an 80% gate in CI, hit it on day one, and never look at the coverage report again. The gate stops being a safety mechanism and becomes a vanity metric — real uncovered branches accumulate unnoticed until one of them fires in production.

From the field

A Wellington fintech team I worked with had 87% branch coverage on their loan-repayment engine — well above the 80% CI gate — and were proud of it. During a pre-release audit we pulled up the coverage report together and found that the entire set of uncovered branches clustered in two areas: the early-exit paths when a direct-debit fails, and the logic that handles partial overpayments. Both are edge cases that almost never happen in a test environment, and both are exactly what happens when a customer's bank rejects a payment on the last Friday before Christmas. We blocked the release, wrote the missing tests in an afternoon, and found two genuine defects — one that would have silently zeroed a repayment balance. The lesson that generalises: always ask where your uncovered branches live, not just how many there are.