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.

graph TB PC[Path Coverage
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
Pathis_memberhas_couponResult
1TrueTrue85
2TrueFalse90
3FalseTrue95
4FalseFalse100

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

AdvantagesLimitations
Tests every code path, not just requirementsCannot find missing functionality (only tests what exists)
Finds dead code and unreachable branchesRequires source code access and programming knowledge
Helps optimize code by revealing complexityTest maintenance is expensive when code changes frequently
Can be automated with coverage tools100% coverage does not guarantee bug-free software
Catches logic errors invisible to black-box testsDoes not validate user requirements or usability

Coverage Tools by Language

LanguagePopular Tools
JavaJaCoCo, Cobertura, Emma
JavaScript/TypeScriptIstanbul/nyc, c8, Vitest coverage
PythonCoverage.py, pytest-cov
C#dotCover, OpenCover
GoBuilt-in go test -cover
RubySimpleCov

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.

HintFor 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:

  1. The for loop iterates once per character. A password of length N creates different paths depending on which characters are uppercase/digits.
  2. 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.
  3. Combined with the two if checks 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