Test Design Techniques — White-Box

White-box testing uses code structure to guide test design. Learn statement coverage, branch coverage, and path coverage — and why 100% coverage doesn't mean bug-free code.

📚 This is Part 7 of the ISTQB Foundation Level series. In the previous post we covered black-box test design techniques. Now we look inside the box.

1. Introduction — Looking Inside the Box

In the previous post, we treated the system as a black box: inputs go in, outputs come out, and we don’t look at the internals. Black-box techniques are powerful precisely because they work from specifications — they tell you what the system should do.

But black-box testing has a blind spot. It can only test what the specification describes. If there’s code in the implementation with no corresponding requirement — a forgotten edge case, dead code, an undocumented default — black-box tests will never reach it.

This is where white-box testing comes in. Instead of deriving tests from requirements, you derive them from the structure of the code itself. The box is now transparent — you can see every statement, every branch, every path.

Key differences:

DimensionBlack-BoxWhite-Box
Based onRequirements / specificationsCode structure
Requires code accessNoYes
Typical test levelSystem, acceptanceUnit, integration
DetectsBehavioural defectsStructural defects
Coverage metricScenarios coveredCode elements executed

White-box testing is primarily used at the unit testing and integration testing levels, where developers write tests against their own code. It is less common at higher test levels, where the implementation may span multiple services.

ISTQB Foundation defines three white-box coverage criteria:

  1. Statement Coverage
  2. Branch (Decision) Coverage
  3. Path Coverage

We will use a single code example throughout this post to make the comparison concrete.


The Running Example

We’ll use this pseudocode discount function throughout:

function discount(price, age):
    if age >= 65:
        return price * 0.8      // 20% senior discount
    if price > 100:
        return price * 0.9      // 10% high-value discount
    return price                // no discount

This function has:

  • 5 executable statements (both if conditions count as statements, plus the three return statements)
  • 3 branches (True/False for each if, plus the fall-through to the final return)
  • Multiple distinct paths depending on which conditions are true

2. Statement Coverage (ISTQB 4.3.1)

Concept

Statement coverage measures whether every executable statement in the code has been executed at least once.

Formula: Statement Coverage = (Statements executed ÷ Total statements) × 100%

A test suite achieving 100% statement coverage has run every line of code at least once.

Applying to Our Example

The discount function has 5 executable statements. How many test cases do we need to execute all of them?

Test Case 1: price = 120, age = 70

  • if age >= 65 → True
  • return price * 0.8 ✅ executed
  • Result: 96

Test Case 2: price = 120, age = 30

  • if age >= 65 → False
  • if price > 100 → True
  • return price * 0.9 ✅ executed
  • Result: 108

Test Case 3: price = 50, age = 30

  • if age >= 65 → False
  • if price > 100 → False
  • return price ✅ executed
  • Result: 50

All 5 statements executed. Statement coverage: 100%.

Limitations

Statement coverage is the weakest white-box criterion. Consider what happens if you only run Test Cases 1 and 2: statement coverage is still 100%, but you have never tested the case where age < 65 AND price <= 100.

Worse: you could execute every return statement while completely missing the False branch of the price > 100 check. Statement coverage gives you no information about untested decision outcomes.

:::tip[ISTQB Exam Tip] Statement coverage is a minimum baseline, not a quality goal. ISTQB treats it as the weakest white-box criterion. 100% statement coverage can be achieved while leaving many important decision paths untested. :::


3. Branch Coverage / Decision Coverage (ISTQB 4.3.2)

Concept

Branch coverage (also called decision coverage) requires that every branch of every decision point is executed — both the True outcome and the False outcome of each if, while, switch, etc.

Branch coverage subsumes statement coverage: achieving 100% branch coverage automatically achieves 100% statement coverage. The reverse is not true.

Applying to Our Example

The discount function has two if decisions, each needing a True and a False test case:

Test Case 1: price = 120, age = 70

  • if age >= 65True

Test Case 2: price = 120, age = 30

  • if age >= 65False
  • if price > 100True

Test Case 3: price = 50, age = 30

  • if age >= 65 → False (already covered)
  • if price > 100False

Three test cases achieve 100% branch coverage. Crucially, branch coverage guarantees you’ve tested what happens when every condition is false — something statement coverage alone does not ensure.

Industry Practice

Branch coverage is the de facto standard for unit testing. Measurement tools are available for every major platform:

  • C#/.NET: Coverlet — run with dotnet test --collect:"XPlat Code Coverage"
  • JavaScript/TypeScript: Istanbul / NYC — integrated with Jest and Vitest
  • Java: JaCoCo
  • Python: coverage.py

Most teams enforce a branch coverage minimum (commonly 70–80%) as a CI/CD quality gate.

:::tip[ISTQB Exam Tip] ISTQB uses the term “decision coverage” for what developers typically call “branch coverage” — they refer to the same thing. The key exam fact: branch/decision coverage subsumes statement coverage. If you have 100% branch coverage, you automatically have 100% statement coverage. :::


4. Path Coverage (ISTQB 4.3.3)

Concept

Path coverage requires that every possible execution path through the code is exercised. A “path” is a unique sequence of branches from function entry to exit.

For the discount function, there are three distinct paths:

  1. age >= 65 is True → return price * 0.8
  2. age >= 65 is False → price > 100 is True → return price * 0.9
  3. age >= 65 is False → price > 100 is False → return price

Three test cases cover all paths in this small function. But consider what happens with more complex code.

Why Path Coverage Is Usually Impractical

The number of paths grows exponentially with the number of decision points:

Decision pointsMaximum paths
532
101,024
201,048,576
Any loopPotentially infinite

For any non-trivial function, 100% path coverage is combinatorially infeasible. It is a theoretical ideal, not a practical target.

MC/DC — A Practical Alternative

In safety-critical systems (aviation following DO-178C, automotive following ISO 26262), a criterion called Modified Condition/Decision Coverage (MC/DC) provides much of the rigour of path coverage at a fraction of the test count.

MC/DC requires that each individual condition in a decision independently affects the outcome. ISTQB Foundation Level requires only awareness that MC/DC exists; it is covered in depth at Advanced Level.

:::tip[ISTQB Exam Tip] For the Foundation exam, remember the hierarchy: path coverage is the strongest but least practical criterion. ISTQB expects you to know that Statement ⊂ Branch ⊂ Path — each criterion subsumes the previous one. :::


5. The Code Coverage Myth

Here is a statement you will encounter frequently in software teams:

“We have 80% code coverage. We’re in good shape.”

Let’s examine why this may be less reassuring than it sounds.

:::danger[Critical Misconception] 100% code coverage does not mean your code is correct. Coverage measures which lines were executed, not whether the behaviour was verified. You can achieve 100% statement coverage with zero assertions and catch zero bugs. :::

Consider this test suite:

// Achieves 100% statement coverage. Catches zero bugs.
test("discount function runs without crashing") {
    discount(120, 70);   // no assertion — result ignored
    discount(120, 30);   // no assertion — result ignored
    discount(50, 30);    // no assertion — result ignored
}

Every statement is executed. No behaviour is verified. The function could return garbage values and this test suite would pass with flying colours.

What Coverage Actually Tells You

Coverage is a diagnostic tool, not a quality metric:

  • High coverage tells you: the code was executed during testing
  • High coverage does NOT tell you: the behaviour was correct, all scenarios were tested, the assertions were meaningful

Coverage is best used to find gaps — areas of code that were never executed, suggesting tests are missing. It is a useful floor (“we should at least run every branch”) but a misleading ceiling (“once we run every branch, we’re done testing”).

Using coverage as a KPI creates perverse incentives: teams write tests that execute code without verifying behaviour, purely to hit a target number.

Better framing: Ask “what scenarios does our test suite cover?” alongside “what percentage of branches does it execute?” Both questions matter; neither is sufficient alone.


6. Combining White-Box and Black-Box

The most effective testing strategy uses both approaches together, at the right levels.

The recommended workflow:

  1. Write black-box tests first (from requirements): Derive test cases from specifications using EP, BVA, decision tables, and state transitions. These tests are implementation-independent — they remain valid if the code is rewritten from scratch.

  2. Run coverage analysis (white-box lens): After writing black-box tests, measure branch coverage. Any code not covered by your black-box tests signals a gap: a missing requirement, an untested edge case, or dead code.

  3. Add targeted white-box tests for structural gaps: Write additional tests to hit uncovered branches and paths. These tests are implementation-specific but ensure the actual code is exercised.

This workflow produces a test suite that is requirement-driven (every test has a clear reason to exist) and structurally complete (the implementation is actually exercised).

Typical approach by test level:

Test LevelPrimary ApproachCoverage Target
Unit testsWhite-box + Black-boxBranch coverage ≥ 70–80%
Integration testsBlack-box (primarily)Statement coverage
System testsBlack-box (exclusively)Scenario coverage
Acceptance testsBlack-box (exclusively)Business scenario coverage

7. Conclusion

White-box techniques provide a complementary lens to black-box testing — they ensure the implementation is tested, not just the specification.

The coverage hierarchy:

Path Coverage          (strongest — impractical for real code)
      ↑ subsumes
Branch / Decision Coverage    (industry standard for unit tests)
      ↑ subsumes
Statement Coverage     (minimum baseline)

Key takeaway: Coverage is a diagnostic, not a guarantee. 80% branch coverage with meaningful assertions is far more valuable than 100% statement coverage with none.

Action: Run a coverage report on your current project (dotnet test --collect:"XPlat Code Coverage" or jest --coverage). Find the five functions with the lowest branch coverage. For each one, ask: is this gap intentional, or is there a scenario worth testing?


This is Part 7 of the ISTQB Foundation Level series.