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

White Box · Structural

Path Coverage

Path coverage tests every possible execution path from a module’s entry point to its exit. It is theoretically the strongest structural coverage criterion — but exponential path growth makes full coverage impractical for most real code. Basis path testing provides a practical, mathematically grounded subset.

Test Lead ISTQB CTFL 4.3 · CTAL-TA 4.4

1 The Hook

A payroll team at a NZ employer tests a function that works out an employee's take-home pay. There are three independent decisions in it: KiwiSaver opt-in, student-loan deduction, and a child-support order. Each of those individually is tested true and false, so branch coverage hits 100%. Everyone is satisfied.

Then a worker who is on KiwiSaver, repaying a student loan, and under a child-support order gets a pay slip that is plainly wrong — the three deductions interact in a combination no test ever ran. Branch coverage checked each decision on its own, but it never walked the specific sequence where all three fire together. That sequence is a path, and it was one of eight through the function. Only a handful had ever been exercised.

This is what path coverage is about: not each decision in isolation, but each end-to-end route through the code. The trap is the other direction too — three decisions already give eight paths, and add a loop and the number becomes unbounded. So the real skill is knowing when to chase paths and when to fall back to a mathematically chosen subset.

💬
Senior Engineer Insight

The danger nobody warns you about is basis paths that silently stop testing what you think they test. You draw the control flow graph, derive your V(G) = 6 paths, write the test cases — then a developer refactors the function three months later. The tests still pass. Every single one. But the paths they were designed against no longer exist; the refactoring collapsed two branches into one and opened a new combination nobody has a test for. I have seen this exact scenario on a NZ government payroll system where the “refactor for readability” introduced a deduction interaction that sat untested for eighteen months. Re-derive your basis paths whenever control flow changes. Treat them like living artefacts, not a one-time deliverable.

2 The Rule

Path coverage tests every end-to-end sequence of decisions from entry to exit — the strongest structural criterion, but it explodes (N decisions give up to 2ⁿ paths, loops give infinitely many). In practice use basis path testing: V(G) linearly independent paths, where V(G) is the cyclomatic complexity, which still achieves full branch coverage.

3 The Analogy

Analogy

Driving every possible route from Auckland to Hamilton, not just every road.

Branch coverage is making sure every individual road segment between Auckland and Hamilton has been driven at least once. Path coverage is driving every complete route — every combination of motorway, off-ramp, and back road that gets you from start to finish. There are far more whole routes than there are road segments, because each junction multiplies the possibilities. Throw in a roundabout you can loop around any number of times and the number of distinct routes becomes endless.

Basis path testing is the sensible compromise: pick a small set of routes that between them cover every road segment and every turn, where each chosen route adds at least one stretch the others did not. You prove the network works without driving every conceivable journey.

What it is

A path is a unique sequence of statements from a module’s entry point to its exit, following specific branches at every decision point along the way. Path coverage is achieved when every such sequence has been executed at least once by the test suite.

Path coverage subsumes all other structural coverage criteria: if every path is covered, then every statement, every branch, and every condition has been exercised. It is the theoretical ceiling of white-box coverage.

The practical problem is path explosion. Each additional independent decision in a function doubles the number of paths. A function with 10 sequential binary decisions has 210 = 1,024 paths. Add a loop that can execute 0 to N times, and the number of paths becomes infinite. This is why the ISTQB Foundation syllabus flags full path coverage as “usually impractical” and why the Advanced syllabus teaches basis path testing as the workable substitute.

White-box coverage hierarchy

The four major white-box coverage criteria form a strict subsumption hierarchy:

  1. Statement coverage — weakest. Every executable statement executed at least once.
  2. Branch coverage — every branch from every decision point taken at least once (true and false).
  3. Condition/MC/DC coverage — each atomic condition independently exercised both ways.
  4. Path coverage — strongest. Every unique execution path from entry to exit.

Each higher criterion subsumes all lower ones: 100% path coverage guarantees 100% branch coverage, which guarantees 100% statement coverage. The converse is not true. A suite achieving 100% branch coverage may cover only a fraction of all paths.

Cyclomatic complexity (McCabe’s metric)

Cyclomatic complexity (V(G), introduced by Thomas McCabe in 1976) is a software metric that quantifies the number of linearly independent paths through a module. It is calculated from the control flow graph using the formula:

V(G) = E − N + 2P

Where:

  • E = number of edges (arrows/transitions) in the control flow graph
  • N = number of nodes (processing steps) in the graph
  • P = number of connected components (usually 1 for a single function)

A simpler equivalent for structured code: V(G) = number of binary decisions + 1. Each if, while, for, case, &&, and || in a compound condition adds one to the count.

Cyclomatic complexity gives the minimum number of test cases needed for basis path testing. It is also used as a code quality metric: functions with V(G) > 10 are generally considered too complex and candidates for refactoring.

Basis path testing

Rather than testing all possible paths (exponential), basis path testing tests a linearly independent set of paths that, taken together, cover every statement and every branch at least once. The number of paths in this basis set equals the cyclomatic complexity V(G).

Linearly independent means: each path in the basis set introduces at least one new edge (branch) not found in any other path in the set. These paths span the “basis” of all possible paths — any other execution path through the code can be expressed as a linear combination of the basis paths.

How to identify basis paths:

  1. Draw the control flow graph for the function.
  2. Calculate V(G) using the formula above.
  3. Identify a baseline path (typically the main/happy path).
  4. Modify the baseline by flipping one decision at a time to create V(G)-1 additional paths, each new path differing from the others by at least one edge.
  5. Design a test case for each path in the basis set.

Worked example

Consider a function that calculates a discount:

function calcDiscount(customer, orderTotal) {
  let discount = 0;                    // Node 1
  if (customer.isVIP) {                // Decision D1
    discount = 0.15;                   // Node 2
  }
  if (orderTotal > 100) {             // Decision D2
    discount = discount + 0.05;        // Node 3
  }
  return discount;                     // Node 4
}

Control flow graph: 4 nodes, D1 and D2 each add 2 edges (true branch + false branch). Total edges E = 6, nodes N = 4, P = 1. V(G) = 6 − 4 + 2 = 4. Four linearly independent paths need to be tested.

calcDiscount — basis paths and test cases
Path Decisions taken isVIP orderTotal Expected discount
P1 (baseline) D1=false, D2=false false $50 0%
P2 D1=true, D2=false true $50 15%
P3 D1=false, D2=true false $150 5%
P4 D1=true, D2=true true $150 20%

Four test cases cover all basis paths. For this two-decision, loop-free function, full path coverage happens to equal the basis set — there are exactly four distinct paths and the cyclomatic complexity is four. For functions with loops, the basis set remains finite (one path per loop zero-iterations, one per loop one-or-more-iterations) even though full path coverage would be infinite.

When path coverage is practical

Full path coverage is practical only in specific circumstances:

  • Safety-critical small modules — DO-178C Level A (aviation), ISO 26262 ASIL D (automotive), and IEC 61508 SIL 4 (industrial safety) require structural coverage at or exceeding MC/DC. For small, loop-free functions in these systems, full path coverage is achievable and mandated.
  • High-cyclomatic-complexity refactoring targets — if a function’s V(G) is 3–5 and it is critical business logic, full path coverage is achievable (8–32 test cases) and worth the investment.
  • Basis path testing as the default — for all other white-box work, use basis path testing. It is always practical (V(G) test cases), achieves 100% branch coverage, and is mathematically complete with respect to the independent paths.

Loops require special handling. A loop that executes 0, 1, or many times represents three paths, not one. Test: zero iterations (skip the loop entirely), one iteration, and a representative many-iterations case. This is sufficient for most loops; for safety-critical loops with known maximum bounds, also test the maximum.

ISTQB mapping

ISTQB reference
Syllabus refTopicLevel
CTFL 4.3Path coverage mentioned as impractical for most systems — awareness onlyFoundation
CTAL-TA 4.4Basis path testing — control flow graphs, cyclomatic complexity, deriving basis pathsAdvanced / Senior
CTAL-TA 4.4 K4Analyse code to draw a control flow graph, calculate V(G), and identify basis pathsAdvanced LO

Foundation candidates are expected to know that path coverage exists, that it subsumes branch coverage, and that it is generally impractical. Advanced (CTAL-TA) candidates must be able to draw a control flow graph, calculate cyclomatic complexity, and identify a set of basis paths with corresponding test cases.

Common mistakes

  • Confusing path coverage with branch coverage — branch coverage tests each decision outcome; path coverage tests each sequence of outcomes through the function. A test suite can achieve 100% branch coverage while covering only a fraction of paths.
  • Attempting full path coverage on code with loops — a single loop with an unbounded iteration count creates infinitely many paths. Use basis path testing and add specific loop boundary tests (0, 1, max iterations) rather than pursuing full coverage.
  • Miscounting cyclomatic complexity — remember that compound conditions (A && B, C || D) add to the count. Each boolean operator adds one to V(G). Many teams count only explicit if/while/for statements and undercount.
  • Not updating basis paths after refactoring — when code is refactored, the control flow graph changes and the basis paths change. Test cases designed for the old paths may no longer correspond to any actual path in the new code.
  • Equating high cyclomatic complexity with many required tests — V(G) is the minimum for basis path testing. It does not mean you should write exactly V(G) tests and stop. You still need black-box tests, edge-case tests, and integration tests on top.

4 Industry Reality

🏭 What you actually encounter on the job
  • Coverage tools report lines, not paths. Most CI pipelines show statement or branch coverage percentages. When you raise path coverage in a sprint retrospective, expect blank looks — you will need to explain cyclomatic complexity in plain language before the conversation goes anywhere.
  • Legacy codebases routinely have V(G) > 20 in critical modules. NZ government and banking systems built in the 2000s often contain multi-hundred-line functions that were never refactored. Full path coverage is impossible; your job is to calculate V(G), identify the highest-risk basis paths, and document which paths are untested and why.
  • Requirements rarely tell you what the paths are. Business analysts specify happy-path scenarios. You have to draw the control flow graph yourself from the code or from a developer walkthrough — often discovering decision points the requirements never mentioned (null checks, silent exception catches, feature flags).
  • Time pressure turns basis path testing into risk-based sampling. In a two-week sprint you will rarely get sign-off for V(G) = 8 tests on every function. Senior testers negotiate: flag which functions are safety-critical or financially sensitive, apply full basis-path analysis there, and use branch coverage elsewhere. That negotiation needs to be in your test plan.
  • Cyclomatic complexity is a code-quality lever, not just a test metric. When V(G) > 10, the most valuable thing you can do is work with the developer to refactor before testing. A function with V(G) = 15 that is split into three functions of V(G) = 5 each is easier to test, cheaper to maintain, and less likely to hide interaction bugs — and the total basis paths drop from 15 to 15 (same), but each sub-function is independently testable.

5 When to Use It — and When Not To

⚡ Decision guide

✓ Use it when

  • The function is safety-critical or financially sensitive (payroll deductions, GST calculations, loan eligibility) and has a manageable cyclomatic complexity (V(G) ≤ 10)
  • A bug has been traced to an interaction between two or more conditions — path analysis reveals the untested combination
  • You are preparing an ISTQB CTAL-TA submission and need to demonstrate white-box analysis at the advanced level
  • A refactoring has changed control flow and you need to verify that the basis paths in the new code are all covered by the existing test suite
  • A developer hands you a function with V(G) = 3–5 and says “this is the core business rule” — basis path testing is fast and gives you a coverage argument you can document

✗ Skip it when

  • The function contains unbounded loops — full path coverage is literally impossible; switch to basis path testing plus loop boundary tests
  • V(G) > 10 and refactoring is not on the table — the number of basis paths makes this impractical; use branch coverage and risk-based test selection instead
  • You are doing integration or system testing — path coverage is a unit/component-level technique; at system level the “paths” are end-to-end scenarios, and exploratory testing or use-case testing is more appropriate
  • The code is generated (ORM queries, templated output, UI frameworks) — you do not control its paths and the generator vendor owns coverage
  • A black-box technique already covers the risk — if boundary value analysis or equivalence partitioning gives you confidence in the outcomes, adding path analysis is diminishing returns

Context guide

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

Context Priority Why
Revenue NZ / Benefits NZ financial eligibility rules (KiwiSaver, Working for Families, student loan) Essential Multiple independent conditions interact in a single calculation; interaction bugs produce incorrect payments affecting real New Zealanders. V(G) is usually manageable (3–8); basis-path testing is both practical and the minimum defensible standard.
HealthNZ / CoverNZ safety-critical modules (dosing calculators, entitlement engines) Essential Patient safety and legislative obligations (Health and Disability Commissioner Act) require you to demonstrate which paths were tested and which were not. Document untested paths with explicit risk ratings in the test report.
TransitNZ / Pacific Air safety systems (booking logic, incident reporting workflows) High CAA and TransitNZ audit trails require demonstrable coverage rationale. Basis-path analysis gives you a documented, repeatable argument for the regulator. Full path coverage is usually unnecessary; the basis set suffices.
Harbour Bank / Spark customer-facing APIs (pricing engines, discount rules, billing logic) Medium Billing errors are visible and reputationally costly, but the codebases are large and V(G) > 10 is common. Apply basis-path analysis to the core pricing kernel; use branch coverage elsewhere and flag any function with V(G) > 10 as a refactoring candidate before testing.
LandNZ / Stats NZ data-processing pipelines (geocoding, census aggregation) Low Pipelines are often loop-heavy and data-driven; full path coverage is structurally impossible. Equivalence partitioning on input classes, boundary value analysis on edge cases, and output-comparison testing deliver more value than attempting structural path analysis here.
Vendor-supplied or generated code (ORM layers, GST library, templating engine) Low You do not own the control flow graph and cannot meaningfully derive basis paths. Test at the integration boundary: verify the outputs your code depends on, not the vendor's internal paths. Effort is better spent on integration and contract testing.

Trade-offs

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

Advantage Disadvantage Use instead when…
Catches interaction bugs that branch coverage misses — the class of defect most common in payroll and eligibility engines where multiple conditions fire together. Path count explodes exponentially with each added decision (N decisions = up to 2N paths). Any loop with an unbounded iteration count makes full path coverage impossible. Use branch coverage when V(G) > 10 and refactoring is not feasible — it is the practical ceiling for most large legacy codebases.
Basis path testing (V(G) tests) always achieves 100% branch coverage as a by-product, so you get two coverage criteria for the price of one analysis. Requires drawing and reasoning about control flow graphs — a skill many teams lack. Miscounting compound conditions (&&, ||) is endemic and produces false confidence in coverage. Use MC/DC when the risk is in the internal logic of a compound condition rather than in the sequence of decisions (e.g. HealthNZ dosing condition with three sub-expressions).
Cyclomatic complexity doubles as a code-quality signal — functions with V(G) > 10 are objectively too complex, giving you a data-backed refactoring conversation with the development team. Basis paths become stale immediately after any structural refactoring. Re-deriving them every sprint on a frequently-changing module is a recurring overhead teams often skip — leaving old tests tracing non-existent paths. Use exploratory or scenario-based testing when the code is a third-party or generated component you cannot structurally analyse.
Provides a defensible, auditable coverage artefact — the control flow graph and basis-path table can be submitted to regulators (CAA, RBNZ, Medsafe) as evidence of systematic structural testing. V(G) is a minimum test count, not a total test budget. Teams that stop at V(G) tests still leave interaction and boundary-value gaps uncovered — creating a false sense of completeness. Use equivalence partitioning and boundary value analysis when the decision points are straightforward but the input ranges carry the most risk — the data, not the structure, is where bugs hide.

Enterprise reality

How Path Coverage changes at 200–300-developer scale in NZ enterprise

  • Automation replaces manual graph derivation. At Pacific Bank, control flow graphs and V(G) counts are generated automatically by SonarQube on every pull request — any function exceeding V(G) = 10 fails the quality gate and cannot merge until refactored or formally exempted. Testers never draw graphs by hand; they interpret the tool output and own the decision to escalate or accept the risk. The skill shifts from drawing CFGs to reading coverage reports critically and writing compelling risk exemptions.
  • The Privacy Act 2020 and NZISM make untested paths a compliance artefact. Under the Privacy Act 2020 Information Privacy Principle 5 (storage security) and the NZ Information Security Manual (NZISM 3.7), agencies processing personal information must demonstrate that access-control logic has been systematically tested. An untested path through an entitlement or authorisation function is not just a quality gap — it is a recordable compliance risk. Enterprise QA teams at Benefits NZ and Revenue NZ maintain path coverage matrices as evidence artefacts submitted to internal audit, not as internal test documentation only.
  • Tooling at volume means multiple instruments in parallel. Large NZ organisations running Java or .NET stacks typically combine JaCoCo or Coverlet (line/branch coverage in CI), SonarQube or NDepend (cyclomatic complexity thresholds and V(G) trend graphs), and contract-test frameworks like Pact (to verify path behaviour at integration boundaries). At TechServNZ, path coverage tooling is wired into Jira so that any module breaching the V(G) threshold automatically generates a tech-debt ticket linked to the offending file and sprint — testers do not need to file it manually.
  • Cross-squad coordination turns a missed path into a production incident. Across 10+ squads sharing a monorepo or microservices mesh, a single untested path in a shared pricing or authorisation library can cascade across every consumer. At CloudBooks, where dozens of squads share core billing logic, an untested interaction path in a discount-stacking module once produced incorrect invoices for roughly 1,200 NZ small businesses in a single overnight billing run — caught by a customer complaint, not the test suite. The engineering standard embedded afterwards: shared libraries must hold all basis-path tests within the library repo itself, never delegated to consuming squads, and any V(G) increase requires a test-plan update approved by the platform team before merging.

What I would do

Professional judgment — when to reach for path coverage, when to skip it, and what to watch for.

📋 Scenario 1 — Revenue NZ KiwiSaver + student loan + child support function
Situation
I am reviewing a net-pay function at a large NZ payroll bureau. It has three independent if deductions — KiwiSaver, student loan, child support — and no loops. The developer has written two happy-path tests and says “it’s simple logic.” The sprint ends Friday.
I would
Calculate V(G) immediately: three decisions gives V(G) = 4. Draw the control flow graph — even a rough whiteboard sketch — and show the developer the four basis paths. Explain that two tests is one short of the minimum. I would then check: does the code correctly accumulate when all three deductions fire together? That is Path 4 and is the most likely real-world scenario for affected workers. I would write the four basis-path tests, tag each with its path, and note in the test report that full path coverage (8 paths including the fourth combination) was not pursued because the basis set achieves 100% branch coverage and the fourth interaction case is separately verified. Friday deadline holds.
📋 Scenario 2 — TransitNZ road-closure notification engine, V(G) = 14
Situation
A TransitNZ routing engine that selects which emergency contacts receive a road-closure notification has V(G) = 14. It is safety-relevant (missed notifications can delay emergency response) but refactoring has been declined for this release. The team wants to apply basis-path testing.
I would
Push back on applying basis-path analysis to the whole function. V(G) = 14 means 14 basis-path tests, each requiring me to trace a different route through a 14-decision graph — that is error-prone and fragile. Instead I would decompose the function into logical sub-components on paper, calculate V(G) per sub-component (each should be ≤ 5 once decomposed logically), and apply basis-path testing to each sub-component separately. I would document the decomposition in the test plan so the TransitNZ audit trail shows a principled approach. Then I would raise a formal tech-debt item for refactoring before the next release, citing V(G) = 14 as objectively too complex by industry standards (McCabe’s original threshold of 10).
📋 Scenario 3 — CoverNZ entitlement eligibility, post-refactor path drift
Situation
An CoverNZ entitlement eligibility function had V(G) = 6 and six well-tagged basis-path tests. A developer refactored it last sprint to “simplify readability” by combining two conditions into a single compound expression. The six tests still pass. The team lead signs off.
I would
Stop the sign-off and re-derive V(G) from the updated code. Combining two if statements into one compound condition with && does not simplify the cyclomatic complexity — it keeps it the same but now the two decision points live inside one expression. However, the paths have changed structure, and two of my six original basis-path tests may now trace the same path through the new control flow graph while a new combination goes unexercised. I would redraw the control flow graph, recalculate V(G), and re-map each existing test to a path in the new graph. Any test that no longer maps to a distinct path gets revised or replaced. I would also add this scenario to the team’s definition of done: “structural refactors require basis-path re-derivation before sign-off.”

The bottom line: Path coverage is not a number you achieve and file — it is a living map of which execution routes through your code have been driven and which have not. Re-derive it whenever the map changes.

6 Best Practices

✓ What experienced testers do
  • ✓ Calculate V(G) before designing any test cases. Knowing the minimum number of basis paths stops you from either over-testing (writing 10 tests when 4 suffice) or under-testing (writing 2 tests and calling it done).
  • ✓ Draw the control flow graph, even a rough sketch. You cannot reliably identify basis paths from reading code alone. A hand-drawn graph on paper or whiteboard takes five minutes and prevents miscounting compound conditions.
  • ✓ Count every && and || in compound conditions. The most common V(G) mistake is counting only if statements. Each boolean operator adds one decision point. Annotate them explicitly on your graph.
  • ✓ Start with the happy path as the baseline. Define the main execution route first, then create each additional basis path by flipping exactly one decision from the baseline. This produces the minimal, non-redundant set.
  • ✓ Tag your test cases with which path they exercise. In your test management tool (or a spreadsheet), record which basis path each test covers. When requirements change and paths change, you can immediately identify which tests need to be updated.
  • ✓ Treat loop boundaries as a separate concern from basis paths. For a loop, add three specific loop tests (zero, one, many iterations) alongside the basis set rather than trying to fold loop variation into basis paths.
  • ✓ Pair path coverage with black-box techniques. Basis path testing guarantees structural coverage but does not guarantee you have the right inputs. Always supplement with equivalence partitioning and boundary value analysis for the inputs at each decision point.
  • ✓ Use V(G) as a refactoring trigger, not just a test count. If you calculate V(G) = 12, escalate to the developer before writing 12 tests. A refactored pair of functions with V(G) = 6 each is better for everyone.
  • ✓ Re-derive basis paths after every structural change. Refactoring, adding a feature flag, or catching a new exception type all change the control flow graph. Your old basis paths may no longer correspond to real paths in the updated code.
  • ✓ Document untested paths explicitly in your test report. When time pressure means not all basis paths are tested, name them and risk-rate them. “P4 (KiwiSaver + student loan + child support) not tested due to sprint constraint — HIGH risk” is far more useful than silence.

7 Common Misconceptions

❌ Myth: “100% branch coverage means we’ve tested all paths.”

Reality: Branch coverage checks that each decision has been taken both ways — true and false — at least once. Path coverage checks every complete sequence of decisions from entry to exit. A suite that exercises each of three decisions independently (true and false) achieves 100% branch coverage with as few as 4 tests, but there are up to 8 distinct paths. The combinations where multiple decisions fire together — exactly the interaction bugs most likely to surface in production — may never be exercised. The NZ payroll bug in the Hook section is a real-world example of this gap.

❌ Myth: “We should always aim for full path coverage to be thorough.”

Reality: Full path coverage is mathematically impossible for any function containing a loop with a non-fixed iteration count, because each iteration count is a distinct path and the count is unbounded. Even for loop-free code, 10 independent decisions produce 1,024 paths. Pursuing full path coverage wastes effort and is not recognised by any mainstream safety standard (DO-178C, ISO 26262, IEC 61508) as the target — they specify MC/DC or structural coverage appropriate to the risk level. Basis path testing achieves 100% branch coverage with V(G) tests and is the recognised practical standard.

❌ Myth: “Cyclomatic complexity tells you exactly how many tests to write, full stop.”

Reality: V(G) gives the minimum number of test cases for basis path testing of the structural paths. It says nothing about black-box test design: you still need tests for boundary values, equivalence classes, error conditions, and business rules that the code structure does not reveal. A function with V(G) = 3 might need 10 or more tests when you combine basis path tests with boundary value analysis on the input ranges. V(G) is a floor, not a ceiling, and treating it as a test budget cap is a common source of undercoverage.

Senior engineer insight

The hardest lesson I ever learned about path coverage is that V(G) is a minimum, not a budget — teams hit their basis-path count, declare victory, and never notice the interaction paths they left untested. What changed how I think about this: once I started mapping paths onto a matrix with decisions as columns and test cases as rows, the untested combinations became immediately visible. A 3×3 grid with two empty rows is impossible to miss; a list of three tests with no coverage annotation is perfectly easy to file and forget.

Most common mistake: teams re-derive cyclomatic complexity after a refactor, update the count, then write the new tests — but never check whether the old tests still map to real paths. After a structural change, half your existing path tests may silently become dead weight while new paths go untested.

From the field

On a safety-critical dosing calculator built for a NZ district health board, the team had V(G) = 5 and five well-documented basis-path tests. Six months after go-live, a maintenance developer added a feature flag to disable the weight-based adjustment for paediatric patients. The control flow graph now had V(G) = 6, but nobody re-ran the basis-path analysis — the new branch was “just a flag.” The untested path was the one where the flag was toggled mid-session by a config reload, producing a dosing calculation that used adult weight thresholds on a paediatric record. It was caught by a sharp-eyed pharmacist, not by the test suite. The generalising lesson: every structural change — even a “safe” feature flag — must trigger a fresh V(G) calculation and a coverage gap review before the build ships.

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: count paths and V(G)

A NZ payroll deduction function has two independent if decisions (no loops, no nesting), shown below. Work out how many complete paths exist, calculate the cyclomatic complexity, and state how many basis-path test cases you need.

function netPay(gross, onKiwiSaver, hasStudentLoan) {
  let net = gross;
  if (onKiwiSaver) {            // D1
    net = net - gross * 0.03;
  }
  if (hasStudentLoan) {         // D2
    net = net - gross * 0.12;
  }
  return net;
}
Show model answer
Total complete paths: 4 (each of the two independent decisions doubles the routes: 2 × 2 = 2² = 4).
Cyclomatic complexity V(G) = decisions + 1 = 2 + 1 = 3.
Basis-path test cases needed: 3 (one per linearly independent path).

Does full path coverage equal the basis set here? No. Full path coverage needs 4 tests (all four combinations of D1/D2). Basis path testing needs only V(G) = 3, because the fourth path can be expressed as a linear combination of the basis paths. The 3 basis paths still achieve 100% branch coverage; the 4th combination (both deductions firing) is the interaction case you may add separately if the deductions interact.
🔧 Exercise 2 of 3 — Fix: repair a miscounted V(G)

A tester calculated cyclomatic complexity for an Revenue NZ eligibility function and concluded 3 tests are enough. The count is wrong because compound conditions were not counted. Recalculate V(G) correctly and state the right minimum.

Flawed working:
"I see two if statements: if (isResident && age >= 18) and if (income < 48000 || hasDependants).
2 decisions + 1 = V(G) of 3. So 3 basis-path tests."

Recalculate correctly:

Show model answer
Boolean operators: one && (in the first if) and one || (in the second if) = 2 extra.
Correct decision count: 2 if-statements + 2 boolean operators = 4 decision points.
Correct V(G) = 4 + 1 = 5.
Minimum basis-path tests: 5, not 3.

What the tester missed: each && and each || adds one to cyclomatic complexity, because a compound condition is really two decisions wired together. Counting only the explicit if statements undercounts V(G) — a very common mistake. The correct minimum for basis path testing here is 5 test cases.
🏗️ Exercise 3 of 3 — Build: handle a loop

A function totals the GST on a list of invoice line items using a loop. Full path coverage is impossible (the loop can run any number of times). Design a practical set of loop tests and explain why full path coverage is not attainable.

function totalGST(lineItems) {
  let gst = 0;
  for (let i = 0; i < lineItems.length; i++) {   // loop
    gst = gst + lineItems[i].amount * 0.15;
  }
  return gst;
}
Show model answer
Why full path coverage is impossible: a loop that can execute 0, 1, 2, ... N times creates a distinct path for every iteration count. With no fixed upper bound, there are infinitely many paths, so full path coverage cannot be achieved.

Practical loop tests (the standard "loop testing" pattern):
- Test 1: empty list (0 iterations) — the loop body never runs; GST should be 0. Tests the skip-the-loop path.
- Test 2: one line item (1 iteration) — the loop runs exactly once.
- Test 3: several line items (many iterations) — a representative N, e.g. 5 items, to test repeated accumulation.

Extra test for safety-critical use: if the loop has a known maximum bound (e.g. invoices capped at 100 lines), also test at that maximum to catch boundary/overflow issues. Together these cover the meaningful loop behaviours without attempting the impossible task of testing every iteration count.

Why teams fail here

  • Treating V(G) as a test budget cap rather than a minimum — writing exactly V(G) tests and shipping, leaving interaction paths untested.
  • Forgetting to count && and || operators when calculating cyclomatic complexity — the most consistent undercount in the field, often by 30–50%.
  • Not re-deriving basis paths after refactoring — old tests pass because the code compiles, but they no longer trace real execution routes through the updated control flow graph.
  • Applying basis path analysis at the system level instead of the unit level — path coverage is a white-box, code-level technique; at system test level it becomes unmeasurable and meaningless.

Key takeaway

Path coverage is not about how many tests you write — it is about knowing which sequences of decisions you have never executed, and making a conscious, documented decision about whether that is acceptable risk.

How this has changed

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

1970s

Path coverage defined alongside other structural coverage criteria in early software testing theory. The goal: exercise every possible path through the code. Immediately recognised as infeasible for any non-trivial program — a loop with 20 iterations has 2^20 paths.

1976

Tom McCabe publishes cyclomatic complexity — a metric for the minimum number of linearly independent paths through a program. Provides a practical approximation to path coverage that is actually measurable.

1990s

Path-based test generation tools are developed for safety-critical domains. Symbolic execution tools attempt to discover all feasible paths through code. NASA and defence contractors use path analysis for critical software. For commercial software, it remains theoretical.

2008

KLEE symbolic execution tool released by Stanford. Concolic testing (concrete + symbolic execution) makes path exploration more feasible. Used for finding security vulnerabilities rather than systematic functional testing.

Now

Fuzzing engines (AFL, LibFuzzer) explore paths through programs far more efficiently than symbolic execution for security testing. AI-assisted program analysis can identify high-risk paths without exhaustive exploration. For business-logic testing, branch coverage remains the practical standard — full path coverage is a theoretical ideal rather than an achievable target.

Self-Check

Click each question to reveal the answer.

Interview Questions

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

Why is 100% path coverage impractical for most software, and what do we use instead?

Strong answer: Path coverage requires exercising every possible execution path through the code. A function with just three independent boolean conditions and a loop has potentially millions of distinct paths — combinatorial explosion makes exhaustive path coverage computationally infeasible. In practice, teams use branch coverage (exercise both outcomes of each decision), cyclomatic complexity as a guide to test case count, and risk-based selection to focus on high-complexity paths. For safety-critical systems, MC/DC provides a practical approximation to path-level coverage that regulators accept.

Junior/Mid

Symbolic execution claims to achieve path coverage automatically. What are its practical limitations?

Strong answer: Symbolic execution explores code paths by treating inputs as symbolic variables and using a constraint solver to find values that reach each path. Practical limitations: path explosion (the number of paths grows exponentially with program size), constraint solver limitations (complex arithmetic, cryptographic operations, and external system calls cannot be modelled), loop handling (symbolic execution cannot enumerate infinite paths through loops), and scalability (it scales to functions and modules, not to large systems). It is most useful for security-focused bug finding (buffer overflows, null dereferences) in bounded code, not for full-system functional testing.

Senior/Lead

Q1: What is a "path", and how does path coverage differ from branch coverage?

A path is a unique sequence of statements from a module's entry to its exit, following specific branches at every decision along the way. Branch coverage tests each decision outcome on its own; path coverage tests each complete sequence of outcomes. A suite can hit 100% branch coverage while covering only a fraction of the paths.

Q2: Why is full path coverage usually impractical?

Path explosion. Each independent binary decision doubles the number of paths, so ten decisions give 2¹⁰ = 1,024 paths. A loop that can run 0 to N times makes the path count effectively infinite. The ISTQB Foundation syllabus flags full path coverage as usually impractical for this reason.

Q3: How is cyclomatic complexity calculated, and what does it tell you about testing?

V(G) = E − N + 2P from the control flow graph, or more simply V(G) = number of binary decisions + 1 for structured code. Each if, loop, case, &&, and || adds one. It gives the minimum number of test cases for basis path testing and is also a code-quality signal — V(G) > 10 suggests a function is too complex.

Q4: What does basis path testing achieve, and how many paths are in the basis set?

It tests a set of linearly independent paths — each adds at least one edge the others do not — that together cover every statement and branch. The number of basis paths equals V(G). Any other path can be expressed as a combination of these, and the basis set achieves 100% branch coverage while staying finite even when full path coverage is infinite.

Q5: How should loops be handled when full path coverage is out of reach?

Use loop testing: test zero iterations (skip the loop), one iteration, and a representative many-iterations case. For safety-critical loops with a known maximum bound, also test that maximum. This covers the meaningful loop behaviours without attempting the impossible task of every iteration count.

Q6: Your team is testing an CoverNZ entitlement eligibility function that has five independent if-decisions and no loops. The sprint is two weeks long and the developer says "just check the happy path and the two obvious failure cases." What do you push back with, and what is the minimum you negotiate for?

A: Calculate V(G) first: five decisions gives V(G) = 6. Point out that "happy path plus two failures" is three tests, which achieves neither branch coverage nor the basis set minimum of six. Negotiate for the six basis-path tests: they take roughly the same time to write once the control flow graph is drawn, they give 100% branch coverage, and they are the minimum needed to catch interaction bugs between conditions — exactly the class of defect most likely to cause an incorrect entitlement decision affecting an injured New Zealander. Document any paths skipped in the test report with a risk rating.

Q7: What is the key difference between path coverage and condition coverage (MC/DC), and when would you choose MC/DC over basis path testing on a safety-critical NZ HealthNZ system?

A: Path coverage tests every complete route through the code from entry to exit — it is about sequences of decisions. MC/DC (Modified Condition/Decision Coverage) tests that each atomic boolean sub-expression independently affects the outcome of the compound decision — it is about the internal logic of individual conditions. On a safety-critical HealthNZ system (e.g. medication dosing logic) where a compound condition like isAdult && weightOver50kg && noContraindication must be verified to be correctly wired, MC/DC is the right standard: it proves each sub-condition matters, which path analysis does not. Path coverage is appropriate at the structural level, MC/DC at the decision-logic level.

Q8: A developer on an Revenue NZ tax-calculation project says "we already have 100% code coverage from our unit tests, so path coverage adds nothing." What is wrong with this claim and how do you respond?

A: "100% code coverage" almost always means 100% statement coverage — every line executed at least once. Statement coverage makes no guarantees about decision outcomes or execution sequences. A test suite can hit every line while never exercising the path where two conditions fire together, which is often where interaction bugs hide (as the KiwiSaver/student-loan/child-support example illustrates). Respond by asking which coverage metric the tool reports, then demonstrating that their current tests may achieve only 60–70% branch coverage once you check. For tax logic with financial consequences, basis path testing is the minimum white-box standard you should argue for.

Q9: When is it appropriate to skip basis path testing entirely and rely on black-box techniques instead, even for complex business logic?

A: Skip basis path testing when the code is generated or third-party (e.g. an ORM layer or a vendor-supplied GST calculation library) — you do not control its paths and cannot meaningfully draw its control flow graph. Also skip it when a strong black-box technique already covers the risk: if boundary value analysis and equivalence partitioning on a KiwiSaver contribution rate function give you full confidence in the decision boundaries, adding path analysis is diminishing returns. The deciding question is whether structural analysis reveals risks the black-box tests miss; if it does not, the effort is better spent on exploratory or integration testing.

Path coverage is the top of the white-box hierarchy. Its practical substitute, basis path testing, requires the same foundation as Branch Coverage — understand and achieve branch coverage first.

Cyclomatic complexity is both a test planning metric (minimum test cases needed) and a code quality metric. Functions with V(G) > 10 are candidates for refactoring before testing. Condition Coverage and MC/DC are the appropriate targets for safety-critical decisions within complex functions.

Practice this technique: Try Test Lead Practice 07 — Test coverage gaps.