White-Box Testing and Code Coverage

White-box testing derives and measures tests from the internal structure of the test object, most often its code. Instead of asking only "what should the software do?", you ask "which parts of the code did my tests run, and which input would run the rest?". Example: no test has ever taken the true side of an if; you read the condition, choose an input that makes it true, and take the expected result from the requirement.

Code coverage is the measurement behind it: a tool records which statements, branches and functions ran during the tests and marks the rest line by line.

Why it matters

Black-box tests come from the requirements, so they cannot show how much of the implementation they reached. Code often holds more than the requirement says: error handling, special cases, old rules, a developer's shortcut. If no test runs that code, its defects reach production unseen.

For a tester, coverage turns "have we tested enough?" into concrete questions: which branches did no test reach, and does the requirement say what should happen there? Even testers who never write unit tests use uncovered code to find gaps in the test basis itself.

White-box and black-box: two sources of tests

The ISTQB glossary defines white-box testing as testing based on an analysis of the internal structure of the component or system. Its test basis is the implementation: code, but also architecture, workflows and data flows. Its coverage items, such as the statements or branches of a function, come from that structure, so the tests can be designed only once the design or the code exists.

Black-box techniques such as equivalence partitioning and boundary value analysis work from the specified behaviour, so they can be designed before any code exists and stay valid however it is written. The two views see different things:

  • Strength: white-box testing considers the whole implementation, so it finds defects even when the specification is vague, outdated or incomplete.
  • Weakness: a requirement that was never implemented leaves no code to cover, so the omission stays invisible. Black-box tests from the requirement reveal it.
  • It works without running the code too: in a review or a dry run you trace inputs through the logic.
  • It applies at every test level: branches in component testing, calls between components in integration testing, workflow steps in system testing.
Black-box tests come from the specification and cannot show which code they reached; white-box tests come from the code and cannot see requirements that were never implemented.

Black-box testingspecification-based

Tests from what the software should do

Test basis:
Requirements, user stories, specifications
Coverage items:
Partitions, boundary values
When tests can be designed:
Before any code exists
Finds:
Behaviour that differs from the requirement, and requirements that were never implemented
Cannot see:
How much of the implementation the tests reached

White-box testingstructure-based

Tests from how the software is built

Test basis:
Code, architecture, workflows, data flows
Coverage items:
Statements, branches, conditions, paths
When tests can be designed:
Only once the design or the code exists
Finds:
Defects in code the requirements do not describe, even when the specification is vague or incomplete
Cannot see:
Requirements that were never implemented: no code, nothing to cover

Teams use both: requirement-based tests first, then white-box tests for the code they did not reach.

How code coverage is measured

A coverage tool instruments the code (it adds counters, or asks the runtime to record execution), runs the tests and writes a report: per file, the share of statements or lines, branches and functions that ran, and the source with each line marked as run, not run or partly run. Line coverage is close to statement coverage but not identical: one line can hold several statements.

Criteria differ in strength: statement coverage asks whether every executable statement ran, branch coverage whether every branch ran, such as both the true and the false outcome of each if. The next lessons treat statement, branch, condition and path coverage in turn. Common tools:

  • JavaScript and TypeScript: Istanbul (the nyc command, also behind Jest's --coverage by default) or the V8 engine's built-in coverage, which Jest and Vitest can also use.
  • Python: coverage.py, which measures statements by default and branches with --branch.
  • Java: JaCoCo, whose Java agent is added when the application starts, so it can also record coverage while a server is tested manually.
  • In CI these tools publish the report and can fail the build below a threshold, such as Jest's coverageThreshold or coverage.py's --fail-under.

Designing tests from uncovered code

White-box design rarely starts from an empty page: teams design tests from the requirements, measure their coverage, then close the gaps. For each uncovered item:

  • Read the decision that guards it: what must be true or false to reach it?
  • Work back to an input: which parameter values, data or state make that condition hold? The comparison is itself a boundary, so boundary value analysis helps.
  • Take the expected result from the test basis, never from the code: a test copied from the implementation confirms whatever the code does, defects included.
  • If the requirement does not describe the behaviour, you have found a gap in the test basis: ask first.
  • Rerun the tests: the item should now be covered and the new test should assert the result.
  • If no input can reach the code, report possible dead code instead of forcing coverage.
Closing a coverage gap: read the guarding decision, work back to an input, take the expected result from the test basis, and stop to ask whenever the requirement is silent.
  1. 1Measure the requirement-based testsThe report marks what never ran
  2. 2Read the guarding decisionWhat must be true or false to get there?
  3. 3Work back to an inputParameter values, data or state; the comparison is a boundary

    Branch: No input reaches it: report possible dead codeCaution

  4. 4Expected result from the test basisNever copied from the code

    Branch: Requirement silent: ask before writing the testCaution

  5. 5Rerun and checkItem covered, result asserted

Back to step 2 next uncovered item

Try it: Boundary Value Generator

Worked example: the line no requirement mentioned

Requirement: "Customers earn 1 loyalty point for every whole 10.00 SAR of an order. Gold members earn double points." Below are the developer's function, three black-box tests and the CI coverage report. Math.floor rounds down, so an order of 125.00 SAR earns 12 points.

Line 10 never ran. It gives 5 bonus points to app orders, a rule the requirement does not mention. Instead of copying +5 into an expected result, the tester asks the product owner, who confirms it: every app order earns 5 bonus points, including orders under 10.00 SAR.

  • T4 loyaltyPoints(125, false, true) expects 17 and passes. Statement and branch coverage are now 100%.
  • The confirmed rule also needs T5 loyaltyPoints(5, false, true) → 5. The code returns 0, because the early return on line 3 skips the bonus: a defect on a path that full branch coverage never required.
  • The uncovered line exposed a missing requirement; the answer exposed a defect. The percentage showed neither.
 1  function loyaltyPoints(total: number, gold: boolean, app: boolean) {
 2    if (total < 10) {
 3      return 0;
 4    }
 5    let points = Math.floor(total / 10);
 6    if (gold) {
 7      points = points * 2;
 8    }
 9    if (app) {
10      points = points + 5;
11    }
12    return points;
13  }

Black-box tests                        Expected   Actual   Result
T1  loyaltyPoints(5, false, false)     0          0        passed
T2  loyaltyPoints(125, false, false)   12         12       passed
T3  loyaltyPoints(125, true, false)    24         24       passed

Coverage (CI run)   Statements    Branches    Not run
loyalty.ts          7/8  87.5%    5/6  83%    line 10: if (app) never true

Try it: Decision Table Generator

What coverage does not tell you

The lesson on test coverage covered the basics: executing code is not checking it, and code never written cannot appear as uncovered. When you design tests from coverage, also remember:

  • Whether the expected results are right: a test can assert a wrong value, for example one copied from the code, and still count.
  • Combinations: full branch coverage takes each decision both ways, not every combination. T1 to T4 reached 100% and missed "small order placed in the app".
  • Data-dependent defects: a statement that ran with harmless data can fail with other values, such as a divisor of zero.
  • Code outside the report: Jest, for example, counts only files the tests loaded unless collectCoverageFrom is set; libraries, database procedures, configuration and other services are usually not measured.
  • Non-functional quality: performance, security, usability and accessibility need their own tests.

Try it: Requirements → Test Cases

Common mistakes

  • Copying the expected result from the code under test, so the test confirms the defect instead of finding it.
  • Treating a coverage target as the goal and adding tests that assert nothing.
  • Reading 100% statement coverage as "fully tested" while branches, combinations and data-dependent cases remain open.
  • Covering an undocumented branch silently instead of asking what the behaviour should be.
  • Relying on white-box testing alone: missing requirements leave no code to cover.
  • Reading only the total, not which files and lines are uncovered or which files the report includes.

The QA perspective

On most teams developers own the unit tests and the coverage tool runs in CI on every change. The tester reads the report with the risks in mind: asks for uncovered lines in changed and critical code, checks whether they hide behaviour the requirements do not describe, and reviews whether the covering tests assert the right results. Walking through uncovered branches with a developer is the fastest way in.

When you report coverage, name the criterion and the suite ("unit tests, branch coverage of changed files: 92%, two uncovered branches in refund handling, questions raised"), and track each question an uncovered branch raised: the answer is a missing test, a missing requirement or dead code.

Practice exercise

Close a coverage gap in an account lockout function

Your task

Requirement: "After 5 consecutive wrong passwords the account is locked. A locked account cannot sign in, even with the correct password. A correct password resets the count of wrong attempts." Below are the login function, its three unit tests and the coverage report.

Find the rule that has no test, design a test that runs the uncovered line with the expected result from the requirement, and decide whether it passes. Then list what 100% coverage would still not tell you.

Material
 1  function login(user: User, password: string): string {
 2    if (user.locked) {
 3      return 'LOCKED';
 4    }
 5    if (checkPassword(user, password)) {
 6      user.failedAttempts = 0;
 7      return 'OK';
 8    }
 9    user.failedAttempts = user.failedAttempts + 1;
10    if (user.failedAttempts > 5) {
11      user.locked = true;
12    }
13    return 'WRONG_PASSWORD';
14  }

Unit tests                                     Expected          Result
T1  correct password, failedAttempts = 2       'OK'              passed
T2  wrong password, failedAttempts = 0         'WRONG_PASSWORD'  passed
T3  correct password, user.locked = true       'LOCKED'          passed

Coverage     Statements    Branches    Not run
login.ts     8/9  89%      5/6  83%    line 11
Steps
  1. Map each sentence of the requirement to T1, T2 and T3.
  2. Find the condition that guards line 11 and the state that makes it true.
  3. Write the new test and its boundary neighbour, then trace them through the code: pass or fail?
  4. List what remains unknown at 100% statement and branch coverage.
Show the model answer
Model answer

T3 covers sentence 2, "a locked account cannot sign in". T1 runs the reset on line 6 (sentence 3) besides a successful sign-in, and T2 counts a single wrong password. Nothing covers sentence 1, the lockout after 5 wrong passwords: that rule is lines 10 and 11, and line 11 never ran.

Line 11 needs user.failedAttempts > 5 after the increment on line 9. The 5th wrong password must lock the account, so the test is failedAttempts = 4, wrong password, expect user.locked === true. Do not assert 'WRONG_PASSWORD' for this attempt just because the code returns it: the requirement does not say what the locking attempt returns, so ask. Boundary neighbour: failedAttempts = 3, wrong password, the account stays unlocked.

The neighbour passes, but the new test fails: 4 + 1 = 5 and 5 > 5 is false, so the lock comes only after the 6th wrong password. Report the off-by-one defect with the input, the expected and actual result and line 10; the fix is >= 5.

A test copied from the code (failedAttempts = 5, expect locked) would reach 100% coverage and pass, confirming the defect.

Still unknown at 100%: whether T1 asserts that failedAttempts was reset to 0, whether the count holds across sessions, devices and simultaneous requests, whether checkPassword is correct, and how a locked account is unlocked, which neither requirement nor code describes: a question for the product owner.

Frequently asked questions

Do I need to be a programmer to do white-box testing?

You need to read code well enough to follow its decisions: conditions, loops and early return statements. You do not need to write production code. Many testers start by reading coverage reports and reviewing unit tests with a developer, and build from there.

Is white-box testing the same as unit testing?

No. Unit (component) testing is a test level; white-box testing is a way to derive and measure tests. Its techniques are used most at component level, but they also apply to integration and system testing, and unit tests themselves are often designed black-box from the component's specification.

Is line coverage the same as statement coverage?

Close, but not identical. A line can hold several statements or a whole decision such as a && b ? x : y (x when a && b is true, otherwise y), so the line counts as covered although part of it never ran. Branch figures and the tool's markers for partly covered lines show what line coverage hides.

Can coverage be measured for manual tests?

Yes, if the application runs as an instrumented build or with a coverage agent, such as the JaCoCo agent for a Java server. The report then shows which code a manual or exploratory session reached. That helps find untouched areas, with the same limit as always: it cannot tell whether you noticed a wrong result.

References

Practise with Mutqan tools

Build decision tables from conditions and actions, generate all rule combinations and collapse impossible ones.

Test Design

Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.

Test Design

Convert a list of numbered requirements into traceable test cases with a requirement-to-test matrix.

Test Design

Go deeper

Your progress on this topic

Part of the QA Roadmap