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.

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 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 IRD 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.

Self-Check

Click each question to reveal the answer.

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.

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.