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:
| Dimension | Black-Box | White-Box |
|---|---|---|
| Based on | Requirements / specifications | Code structure |
| Requires code access | No | Yes |
| Typical test level | System, acceptance | Unit, integration |
| Detects | Behavioural defects | Structural defects |
| Coverage metric | Scenarios covered | Code 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:
- Statement Coverage
- Branch (Decision) Coverage
- 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
ifconditions count as statements, plus the threereturnstatements) - 3 branches (True/False for each
if, plus the fall-through to the finalreturn) - 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→ Truereturn price * 0.8✅ executed- Result: 96
Test Case 2: price = 120, age = 30
if age >= 65→ Falseif price > 100→ Truereturn price * 0.9✅ executed- Result: 108
Test Case 3: price = 50, age = 30
if age >= 65→ Falseif price > 100→ Falsereturn 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 >= 65→ True ✅
Test Case 2: price = 120, age = 30
if age >= 65→ False ✅if price > 100→ True ✅
Test Case 3: price = 50, age = 30
if age >= 65→ False (already covered)if price > 100→ False ✅
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:
age >= 65is True →return price * 0.8age >= 65is False →price > 100is True →return price * 0.9age >= 65is False →price > 100is 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 points | Maximum paths |
|---|---|
| 5 | 32 |
| 10 | 1,024 |
| 20 | 1,048,576 |
| Any loop | Potentially 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:
-
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.
-
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.
-
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 Level | Primary Approach | Coverage Target |
|---|---|---|
| Unit tests | White-box + Black-box | Branch coverage ≥ 70–80% |
| Integration tests | Black-box (primarily) | Statement coverage |
| System tests | Black-box (exclusively) | Scenario coverage |
| Acceptance tests | Black-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.