You wrote the code.
Now prove it does what it does.
Black-box testing asks "does the output match the spec?" White-box testing pops the hood and asks "did we actually run every line, every branch, every condition inside it?" This chapter is the toolbox for that: code reviews that catch bugs before execution, coverage metrics that measure how much of the code your tests actually touched, and cyclomatic complexity — the number that tells you when a function has become untestable.
Bending the waterfall into a V
Every development phase has a matching testing mindset. The V-Model just draws that pairing explicitly.
- Do an early test design.
- Involve the right people in the design of the right type of tests.
- Make the tests reflect closely the steps on the left-hand side of the V.
Black box testing — the focus of the next chapter. Unit testing is where white-box techniques live, because it's the level closest to the raw source code.
White-box testing: two very different branches
White-box testing requires access to code. It looks at the program code and performs testing by mapping program code to functionality. It splits into two branches that are easy to conflate but test completely differently: one never runs the program, the other only runs the program.
Static Testing
Involves only the source code, not executables or binaries. Humans go through it, or specialized tools do.
Structural Testing
Runs the product against predefined test cases and compares actual results to expected results.
Why white-box testing at all?
motivation- The program code truly represents what the program actually does — not just what it is intended to do.
- It minimizes delay between defect injection and defect detection (doesn't postpone detection to someone else, downstream).
- It can catch "obvious" programming errors that don't necessarily map to common user scenarios — e.g. a divide-by-zero that only a black-box user story would never trigger.
Reading code without running it
Four flavors, arranged on a single spectrum: more people, more formalism, more complex defects caught.
Static testing checks: whether the code works according to the functional requirement; whether it was written in accordance with the earlier design; whether it follows applicable standards; whether any functionality was missed; and whether the code handles errors properly. Humans can find errors computers can't, multiple humans bring multiple perspectives, and a human evaluation compares code against specs more thoroughly — it detects multiple defects at once and minimizes delay in identifying problems.
Desk Checking
solo · zero formalismThe author informally checks their own code against the specifications and corrects defects found. No structured method or formalism ensures completeness. No log or checklist is maintained — it relies solely on the thoroughness of the author.
- The programmer knows the code and language well — best suited to read the program.
- Less scheduling and logistics overhead.
- Reduces delay in defect detection and correction.
- Person-dependent — not scalable or reproducible.
- Tunnel visioning of developers.
- Developers prefer writing new code, don't like testing.
Code Walkthrough
group · informalGroup-oriented, as against desk checking — brings in multiple perspectives. Less formal than inspection.
Formal Inspection / Fagan Inspection
group · highly formalA group-oriented activity, highly formal and structured, with specific roles and requiring thorough preparation. Named after Michael Fagan (IBM).
- Author — author of the work product, makes required material available to reviewers, fixes reported defects.
- Moderator — controls the meeting(s).
- Inspectors (reviewers) — prepare by reading required documents, take part in meetings and report defects.
- Scribe — takes notes during the meeting, assigned in advance by turns, documents minutes and circulates them.
Author explains their perspective, makes available the necessary documents, and highlights concern areas for which review comments are sought.
Everyone comes prepared. Moderator goes through the code sequentially; each reviewer raises comments. Comments are categorized: "defect" / "observation," "major" / "minor," "systemic" / "mis-execution." Scribe documents and circulates all findings.
- Thorough, when prepared well.
- Brings in multiple perspectives.
- Found to be very effective.
- Logistically difficult.
- Time consuming.
- May not be possible to exhaustively go through the entire code.
Combining Methods & Static Analysis Tools
practical mixHigh priority / important code → formal inspection. Medium priority → walkthrough. Low priority → desk checking. Teams also decide between 100% coverage vs. partial coverage.
- Unreachable code (e.g. from
gotomisuse) - Variables declared but not used
- Mismatch in definition vs. assignment of variables
- Illegal or error-prone type-casting
- Non-portable / architecture-dependent constructs
- Memory allocated with no matching free (leaks)
- Cyclomatic complexity calculation
- Extension of compilers — lint, compiler-flag-driven checking
Running the executable against test data
Structural testing is done by running the executable on a machine: running the product against predefined test cases and comparing results against expected results, designing test cases and test data to exercise certain portions of code. It has three types: unit / code functional testing, code coverage testing, and code complexity testing.
Initial quick checks by the developer. Removes "obvious" errors before more expensive checks. Involves building "debug versions" and running through an IDE. ("Debugging" or "testing"? Who cares!)
Exercises different parts of code and maps them to required functionality. Finds the percentage of code covered by "instrumentation" — a rebuild of the product, linked with special libraries ("plugged in"), that can monitor what parts of the code are covered. Helps identify critical portions of code executed often.
Generate test data that makes the program enter the sequential block and go all the way through it (watch for multiple entry points in non-structured programming). Statement coverage as a metric:
Have data to test the THEN part and data to test the ELSE part.
Total = 0; /* set total to zero */
if (code == "M") {
stmt1; stmt2; stmt3; stmt4;
stmt5; stmt6; stmt7;
}
else percent = value/Total*100; /* divide by zero */
There should be test cases that: skip the loop completely; exercise the loop between once and the maximum number of times; try covering the loop around the "boundary" of n.
Statement, path, condition, function
One running example threads through the rest of this chapter — memorize its shape and every coverage criterion becomes a variation on the same question.
The foo() function (Figure 4.1)
the running examplevoid foo(int a, int b, int x)
{
if (a>1 && b==0) {
x = x/a;
}
if (a==2 || x>1) {
x = x+1;
}
}
| Path | Input | Output |
|---|---|---|
ace | A=2, B=0, X=4 | X=3 |
abd | A=0, B=0, X=0 | X=0 |
acd | A=3, B=0, X=3 | X=1 |
abe | A=2, B=1, X=1 | X=2 |
abd = false branch of decision 1, then false branch of decision 2) — not one
fixed set of inputs. Many different input combinations can trigger the same path.Statement Coverage
weakest criterionA set of test cases that traverses every statement at least once. Statement coverage = (# statements exercised) / (Total # of statements).
ace alone provides full statement coverage of
foo(): test case (A=2, B=0, X=4) → expected output X=3.ace:
- Maybe the first condition should be an OR rather than an AND.
- Maybe the second condition should be
X>0rather thanX>1. - Path
abd(A=1, B=1, X=1) leaves X unchanged — if that's an error, statement coverage never caught it.
Path Coverage
stronger representationStatement coverage may not indicate "true coverage." Path coverage provides a better representation. Path coverage = (# paths exercised) / (Total # of paths in the program).
Condition Coverage
refines path coverageA further refinement of path coverage. Makes sure each constituent condition in a boolean expression is covered — protects against compiler optimizations (e.g. short-circuit evaluation hiding an untested sub-condition).
DO K=0 to 50 WHILE (J+K<QUEST)
has two conditions — K<=50 and J+K<QUEST. Condition
coverage needs 4 test cases: K<=50, K>50, J+K<QUEST, J+K>=QUEST.Function Coverage
highest abstractionFinds how many functions are covered by test cases. A higher level of abstraction — hence it's possible to achieve 100% coverage. More "logical" than the other coverage types.
- Can be derived from an RTM (Requirements Traceability Matrix).
- Easy to prioritize since based on requirements.
- Leads more naturally to black-box tests.
- Can also be a natural predecessor to performance testing.
Five criteria, same code, five different guarantees
Statement, decision, condition, decision/condition, and multiple-condition
coverage — all applied to the same foo() function, each catching a bug the last
one missed.
White-box testing is concerned with the degree to which test cases exercise or cover the logic of the source code. The ultimate white-box test would execute every path — an unrealistic goal for any program with loops. Backing away from "every path," a more reasonable goal is to execute every statement at least once — which is exactly where the coverage ladder starts.
Decision Coverage (Branch Coverage)
criterion 2A stronger logic-coverage criterion than statement coverage. Write enough test cases to cover each decision with both the true and false outcomes at least once. In case of multiple choices in a decision (e.g. a switch statement), cover all choices at least once. Some languages have multiple entry points for a module or program (e.g. PL/1) — the precise definition is to exercise every possible outcome of all decisions at least once and invoke each point of entry at least once.
| Set A (red) | ||
|---|---|---|
ace | A=2, B=0, X=4 | X=3 |
abd | A=1, B=1, X=1 | X=1 |
| Set B (yellow) | ||
acd | A=3, B=0, X=3 | X=1 |
abe | A=2, B=1, X=1 | X=2 |
abd, the only path that catches an "X must be changed" error — so there's
only a 50% chance of picking the set that catches it. And if the second decision were wrong
(should be X<1 instead of X>1), Set B wouldn't detect the
mistake at all. We need a better criterion.Condition Coverage — applied to foo()
criterion 3Write enough test cases so each condition in each decision takes on all possible values at least once, and each point of entry is invoked at least once. A stronger logic-coverage test than decision coverage.
foo(): A>1, B==0 (B==0 in C/Java),
A==2 (A==2 in C/Java), X>1. Test cases must cover: A>1 / A<=1 — B==0 / B<>0 —
A==2 / A<>2 — X>1 / X<=1.When we pick a value for a variable, it may
satisfy more than one condition requirement at once. Just two test cases can cover all eight
requirements: A=2, B=0, X=4 (path ace) and A=1, B=1, X=1
(path abd).
⚠ The trap: those same two test cases fail decision coverage of the OR
Decomposing A=2 OR X>1
into simple predicates IF(A=2) then IF(X>1) reveals: the
false outcome of "B==0" and the true outcome of "X>1" are both missed by
ace + abd. Condition coverage on the whole expression does not
guarantee condition coverage on each simple predicate once it's decomposed.
A different pair — A=2,
B=4, X=4 (path abe) and A=0, B=0, X=1 (path
abd) — also covers all four conditions' true/false values, but path c
(and the statement on it) is never executed by this set. So: condition coverage does
not guarantee decision coverage, and therefore no guarantee of path or statement
coverage either.
Decision/Condition Coverage
criterion 4 — the fixThe solution to condition coverage's gap: select test cases such that every condition outcome, every decision outcome, and every point of entry are exercised at least once.
Multiple-Condition Coverage
criterion 5 — the strongestWrite sufficient test cases so that all possible combinations of condition outcomes in each decision, and all points of entry, are exercised at least once. Multiple condition coverage satisfies condition-, decision-, and decision/condition-coverage criteria simultaneously.
| # | Combination | # | Combination |
|---|---|---|---|
| 1 | A>1 and B=0 | 5 | A=2 and X>1 |
| 2 | A>1 and B<>0 | 6 | A=2 and X<=1 |
| 3 | A<=1 and B=0 | 7 | A<>2 and X>1 |
| 4 | A<=1 and B<>0 | 8 | A<>2 and X<=1 |
acd. That there are 4 test cases and 4 paths
in this program is pure coincidence — there is no inherent relationship between the
number of paths and the number of test cases.Four situations to test: (1) I<=TABSIZE and NOTFOUND true; (2) I<=TABSIZE and NOTFOUND false (found before end of table); (3) I>TABSIZE and NOTFOUND true (hit end without finding); (4) I>TABSIZE and NOTFOUND false (entry is the last one in the table).
if (x==y && length(z)==0 && FLAG)
j=1;
else
i=1;
Three boolean conditions → 2³ = 8 test cases for multiple-condition coverage (x==y, length(z)==0, FLAG each T/F, all combined).
Cyclomatic complexity & independent paths
Two open questions drive this: which of the paths are independent (if paths aren't independent, we may minimize the number of tests needed)? And is there an upper bound on the number of tests needed to ensure every statement executes at least once? Cyclomatic complexity answers both — it's an indication of how many "independent paths" exist in a program.
- Start with a conventional flow chart.
- Identify the "decision points" — each one is a predicate.
- Convert "complex predicates" to "simple predicates."
- If there are loops, convert the loop conditions to simple predicates too.
- Combine all sequential statements into a single node in the flow graph.
- When sequential statements are followed by a simple predicate, combine the statements and the predicate check into one node with two edges emanating out — nodes with two emanating edges are called predicate nodes.
- Make sure all edges terminate in some node — add a node to represent all statements at the end of the program.
Independent Paths & Basis Sets
applying CCAn independent path is a path in the flow graph that has not been traversed before in other paths. A set of independent paths that together cover all the edges is called a basis set. Find the basis set, then write test cases to execute every path in it — that's exactly how many tests you minimally need.
| Cyclomatic Complexity | What it means |
|---|---|
| 1 – 10 | Well-written code, testability is high, cost/effort to maintain is low. |
| 10 – 20 | Moderately complex, testability is medium, cost/effort to maintain is medium. |
| 20 – 40 | Very complex, testability is low, cost/effort to maintain is high. |
| > 40 | Not testable — any amount of money/effort may not be enough. |
Where white-box testing breaks down in practice
"Not finding time"
Developers routinely report not finding time for testing under delivery pressure.
Blind spots
Developers develop blind spots for their own defects — the same misunderstanding that caused the bug also hides it.
Coverage ≠ realism
Fully covered code may not correspond to realistic user scenarios — 100% coverage is not the same as "correct."
Where exam answers actually go wrong
Every one of these has shown up as a graded confusion — drill them until the wrong answer stops sounding plausible.
1. "100% statement coverage means the code is well tested."
Total=0; if(code=="M")...else percent=value/Total*100; example, a test suite
that only ever hits the IF branch gets full statement coverage while the divide-by-zero in
ELSE never runs.2. "Decision coverage covers all individual conditions."
3. "Since condition coverage is 'stronger,' it guarantees decision coverage too."
A=2,B=4,X=4 /
A=0,B=0,X=1 pair covers all four individual conditions but never executes
path c — so condition coverage does not guarantee decision, path, or statement
coverage. You need Decision/Condition coverage to get both guarantees at once.4. "Multiple-condition coverage = full path coverage."
foo() covers all 8
condition combinations but still misses path acd. There is no inherent
relationship between the number of test cases and the number of paths — matching counts
(4 tests, 4 paths) is coincidence, not causation.5. Mixing up the cyclomatic complexity formulas or miscounting predicates.
(A or B)
as one predicate node, or forgetting the "+2" / "+1" term.6. Treating desk checking, walkthrough, and Fagan inspection as interchangeable.
7. "White-box testing means running the code."
8. Assuming a path label maps to one fixed set of input values.
abd is always A=0,B=0,X=0" (or always
A=1,B=1,X=1 — picking whichever slide you saw first).abd with different concrete
values in different examples.Every technique, one scannable grid
| Technique | Formula / Rule | Guarantees | Key weakness |
|---|---|---|---|
| Desk checking | Author self-review, no formalism | Catches "obvious" errors | Not scalable, tunnel vision |
| Code walkthrough | Group, informal | Multiple perspectives | Less structured than inspection |
| Fagan / formal inspection | Group, roles (Author/Moderator/Inspectors/Scribe) | Very effective, thorough | Logistically hard, time-consuming |
| Statement coverage | # stmts exercised / total stmts | Every line runs once | Misses whole untaken branches |
| Path coverage | # paths exercised / total paths | Better representation than statements | Explodes with loops/branches |
| Decision (branch) coverage | Every decision T & F at least once | Usually implies statement coverage | Doesn't guarantee condition coverage |
| Condition coverage | # conditions covered / # conditions | Every individual condition T & F | Doesn't guarantee decision coverage |
| Decision/Condition coverage | Union of the two above | Both decision & condition outcomes | Still not full path coverage |
| Multiple-condition coverage | All 2ⁿ combos per decision | Condition + decision + decision/condition | Still not full path coverage |
| Cyclomatic complexity | CC = E−N+2 = P+1 | # of independent paths / basis set size | Miscounted if predicates aren't simplified first |
Click to reveal
No reload, no external libraries — just click a question to check yourself.
foo(), running only path ace (A=2, B=0, X=4)
satisfies which coverage criterion?X>1 should be X>0.c in foo().foo() still misses path acd.Exam-shaped problems, worked
Same shape of question as the graded exercises in the deck — drill until it's automatic.
foo(int a, int b, int x), give one test case that
achieves 100% statement coverage, and name the path it takes.ace)x=x/a and x=x+1 statements execute — every line runs at least
once.