What Is White-Box Testing?
White-box testing — also called structural testing, glass-box testing, or clear-box testing — is a testing approach where tests are designed based on the internal structure of the software. The tester has full visibility into the source code, architecture, and implementation details.
Think of it like inspecting the inside of a watch. Instead of just checking whether the hands show the correct time (black-box), you examine every gear, spring, and mechanism to verify they function correctly.
White-box testing answers the question: “Does every part of the code work as implemented?”
Who Performs White-Box Testing?
White-box testing is primarily performed by:
- Developers during unit testing — they know the code they wrote
- SDETs (Software Development Engineers in Test) who can read and analyze production code
- QA engineers with coding skills during integration or system testing
It requires access to the source code and the ability to understand it. A tester who cannot read code cannot effectively perform white-box testing.
Code Coverage Techniques
Code coverage measures how much of the source code is exercised by your tests. Different coverage criteria provide different levels of thoroughness.
Every possible path] BC[Branch Coverage
Every decision outcome] SC[Statement Coverage
Every line executed] PC -->|subsumes| BC BC -->|subsumes| SC style PC fill:#ef4444,color:#fff style BC fill:#f97316,color:#000 style SC fill:#22c55e,color:#000
The diagram shows the subsumption relationship: achieving path coverage guarantees branch coverage, which in turn guarantees statement coverage. But not the reverse.
Statement Coverage
Statement coverage (also called line coverage) measures the percentage of executable statements that have been exercised by tests.
Formula: Statement Coverage = (Executed Statements / Total Statements) × 100%
Consider this function:
def calculate_discount(price, is_member):
discount = 0 # Statement 1
if is_member: # Statement 2
discount = price * 0.1 # Statement 3
final_price = price - discount # Statement 4
return final_price # Statement 5
Test 1: calculate_discount(100, True) executes statements 1, 2, 3, 4, 5 → 5/5 = 100% statement coverage
But wait — what if there is a bug that only manifests when is_member is False? Statement coverage does not catch that because it never required testing the else path.
Limitation: 100% statement coverage does not mean 100% tested. It only means every line was reached at least once.
Branch Coverage
Branch coverage (also called decision coverage) measures whether every possible outcome of every decision point has been tested. Each if, while, for, switch, and ternary operator creates branches.
Formula: Branch Coverage = (Executed Branches / Total Branches) × 100%
Using the same function:
def calculate_discount(price, is_member):
discount = 0
if is_member: # Branch: True / False
discount = price * 0.1
final_price = price - discount
return final_price
The if is_member creates two branches: True and False.
Test 1: calculate_discount(100, True) → exercises the True branch
Test 2: calculate_discount(100, False) → exercises the False branch
Both tests together achieve 100% branch coverage (2/2 branches).
Branch coverage is stronger than statement coverage. Achieving 100% branch coverage guarantees 100% statement coverage, but not vice versa.
Path Coverage
Path coverage requires that every possible execution path through a function is tested. A path is a unique sequence of statements from entry to exit.
For a function with two independent if statements, there are 4 paths:
def process_order(is_member, has_coupon):
price = 100
if is_member: # Decision 1
price -= 10
if has_coupon: # Decision 2
price -= 5
return price
| Path | is_member | has_coupon | Result |
|---|---|---|---|
| 1 | True | True | 85 |
| 2 | True | False | 90 |
| 3 | False | True | 95 |
| 4 | False | False | 100 |
With loops, the number of paths can become infinite (loop executes 0 times, 1 time, 2 times, …), making 100% path coverage impractical for most real-world code.
Data Flow Testing
Data flow testing tracks how data values flow through the code. It focuses on the lifecycle of variables:
- Definition (def) — where a variable is assigned a value
- Use (use) — where a variable is read (in a computation or decision)
- Kill — where a variable is undefined or goes out of scope
Common data flow criteria include:
- All-defs: Every definition of every variable is reached by at least one use
- All-uses: Every definition-use pair is covered
- All-du-paths: Every definition-use path is covered
Data flow testing is effective at finding initialization errors, unused variables, and dangling references.
When to Use White-Box Testing
White-box testing is most effective for:
- Unit testing — testing individual functions and methods
- Code refactoring — ensuring behavior is preserved after structural changes
- Security testing — finding vulnerabilities in authentication, encryption, input handling
- Optimization — identifying dead code and unreachable paths
- Regulatory compliance — industries like aviation (DO-178C) and automotive (ISO 26262) require specific coverage levels
Advantages and Limitations
| Advantages | Limitations |
|---|---|
| Tests every code path, not just requirements | Cannot find missing functionality (only tests what exists) |
| Finds dead code and unreachable branches | Requires source code access and programming knowledge |
| Helps optimize code by revealing complexity | Test maintenance is expensive when code changes frequently |
| Can be automated with coverage tools | 100% coverage does not guarantee bug-free software |
| Catches logic errors invisible to black-box tests | Does not validate user requirements or usability |
Coverage Tools by Language
| Language | Popular Tools |
|---|---|
| Java | JaCoCo, Cobertura, Emma |
| JavaScript/TypeScript | Istanbul/nyc, c8, Vitest coverage |
| Python | Coverage.py, pytest-cov |
| C# | dotCover, OpenCover |
| Go | Built-in go test -cover |
| Ruby | SimpleCov |
These tools generate reports showing which lines, branches, and functions were executed during testing, often with visual highlights in the IDE.
Exercise: Calculate Statement and Branch Coverage
Given the following function:
def validate_password(password):
errors = [] # S1
if len(password) < 8: # S2, Branch B1 (T/F)
errors.append("Too short") # S3
if len(password) > 64: # S4, Branch B2 (T/F)
errors.append("Too long") # S5
has_upper = False # S6
has_digit = False # S7
for char in password: # S8, Branch B3 (enter/skip)
if char.isupper(): # S9, Branch B4 (T/F)
has_upper = True # S10
if char.isdigit(): # S11, Branch B5 (T/F)
has_digit = True # S12
if not has_upper: # S13, Branch B6 (T/F)
errors.append("No uppercase") # S14
if not has_digit: # S15, Branch B7 (T/F)
errors.append("No digit") # S16
return errors # S17
Part 1: You run the following test cases:
- Test A:
validate_password("Ab1cdefgh")— valid password (8+ chars, has uppercase, has digit) - Test B:
validate_password("short")— too short, no uppercase, no digit
Calculate the statement coverage and branch coverage achieved by these two tests combined.
Part 2: What additional test cases would you need to achieve 100% branch coverage? List them and explain which branches they cover.
Part 3: Is 100% path coverage feasible for this function? Explain why or why not, and estimate the number of unique paths.
Hint
For Part 1, trace through each test case line by line and mark which statements execute and which branch outcomes occur. Remember that the `for` loop body executes once per character.For Part 2, look at the branch coverage table and find any branch outcomes (True or False) that were not exercised by Tests A and B.
Solution
Part 1: Coverage Calculation
Test A: validate_password("Ab1cdefgh")
- S1: ✅ (errors = [])
- S2: ✅ (len is 9, not < 8) → B1-False
- S3: ❌ (skipped)
- S4: ✅ (len is 9, not > 64) → B2-False
- S5: ❌ (skipped)
- S6: ✅, S7: ✅
- S8: ✅ (loop enters) → B3-Enter
- S9: ✅ → B4-True (for ‘A’), B4-False (for other chars)
- S10: ✅ (for ‘A’)
- S11: ✅ → B5-True (for ‘1’), B5-False (for other chars)
- S12: ✅ (for ‘1’)
- S13: ✅ (has_upper is True) → B6-False
- S14: ❌ (skipped)
- S15: ✅ (has_digit is True) → B7-False
- S16: ❌ (skipped)
- S17: ✅
Test B: validate_password("short")
- S1: ✅
- S2: ✅ (len is 5, < 8) → B1-True
- S3: ✅
- S4: ✅ (len is 5, not > 64) → B2-False
- S5: ❌
- S6: ✅, S7: ✅
- S8: ✅ → B3-Enter
- S9: ✅ → B4-False (all lowercase)
- S10: ❌
- S11: ✅ → B5-False (no digits)
- S12: ❌
- S13: ✅ → B6-True
- S14: ✅
- S15: ✅ → B7-True
- S16: ✅
- S17: ✅
Combined Statement Coverage: Executed: S1, S2, S3, S4, S6, S7, S8, S9, S10, S11, S12, S13, S14, S15, S16, S17 = 16 statements Not executed: S5 only Coverage: 16/17 = 94.1%
Combined Branch Coverage:
- B1: True ✅ (Test B), False ✅ (Test A)
- B2: True ❌, False ✅ (both tests)
- B3: Enter ✅ (both tests), Skip ❌ (never tested empty password)
- B4: True ✅ (Test A), False ✅ (both tests)
- B5: True ✅ (Test A), False ✅ (both tests)
- B6: True ✅ (Test B), False ✅ (Test A)
- B7: True ✅ (Test B), False ✅ (Test A)
Covered: 11 out of 14 branch outcomes Coverage: 11/14 = 78.6%
Part 2: Additional Tests for 100% Branch Coverage
Test C: validate_password("") — covers B3-Skip (empty string, loop never enters) and B2-False (still not > 64)
Test D: validate_password("A" * 65) — covers B2-True (length > 64)
After adding Tests C and D: all 14 branch outcomes are covered = 100% branch coverage.
Note: Test C also covers B1-True (empty string < 8), B6-True, B7-True — but those were already covered.
Part 3: Path Coverage Feasibility
100% path coverage is not feasible for this function because:
- The
forloop iterates once per character. A password of length N creates different paths depending on which characters are uppercase/digits. - For a fixed-length password of N characters, each character can take B4-True or B4-False AND B5-True or B5-False, creating 4 combinations per character × N iterations.
- Combined with the two
ifchecks before the loop (B1, B2) having 2 outcomes each and two after (B6, B7) with outcomes determined by the loop, the total paths grow exponentially with password length.
For a password of length 8: approximately 4^8 = 65,536 paths through the loop alone. This makes exhaustive path testing impractical. In practice, you use boundary-based path selection instead.
Industry Coverage Standards
Different industries mandate different coverage levels:
- DO-178C (Aviation): Level A (catastrophic failure) requires Modified Condition/Decision Coverage (MC/DC), which is even stricter than branch coverage
- ISO 26262 (Automotive): ASIL D requires MC/DC; ASIL A requires statement coverage
- IEC 62304 (Medical devices): Class C software requires branch coverage
- General web/mobile: Most teams target 80% statement coverage as a practical baseline
The key insight is that coverage is a necessary but not sufficient condition for quality. High coverage means you tested a lot of code. It does not mean you tested it well.
Key Takeaways
- White-box testing designs tests based on internal code structure, requiring source code access
- Statement coverage ensures every line executes; branch coverage ensures every decision outcome is tested; path coverage ensures every execution path is tested
- Coverage criteria have a subsumption hierarchy: path > branch > statement
- 100% coverage does not guarantee bug-free software — it only means the code was reached
- White-box testing is essential for unit testing, security analysis, and regulatory compliance
- Coverage tools automate measurement and reporting across all major programming languages