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 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
nyccommand, also behind Jest's--coverageby 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
coverageThresholdor 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.
- 1Measure the requirement-based testsThe report marks what never ran
- 2Read the guarding decisionWhat must be true or false to get there?
- 3Work back to an inputParameter values, data or state; the comparison is a boundary
Branch: No input reaches it: report possible dead codeCaution
- 4Expected result from the test basisNever copied from the code
Branch: Requirement silent: ask before writing the testCaution
- 5Rerun and checkItem covered, result asserted
Back to step 2 next uncovered item
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 earlyreturnon 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 trueWhat 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
collectCoverageFromis set; libraries, database procedures, configuration and other services are usually not measured. - Non-functional quality: performance, security, usability and accessibility need their own tests.
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
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.
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- Map each sentence of the requirement to T1, T2 and T3.
- Find the condition that guards line 11 and the state that makes it true.
- Write the new test and its boundary neighbour, then trace them through the code: pass or fail?
- List what remains unknown at 100% statement and branch coverage.
Show the 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
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
- ISO/IEC/IEEE 29119-1:2022 — Software and systems engineering — Software testing — Part 1: General conceptsThe international software-testing standard: concepts, processes, documentation and techniques.
- NIST SP 500-235: Structured Testing: A Testing Methodology Using the Cyclomatic Complexity MetricStructured (basis path) testing: McCabe's cyclomatic complexity used to derive a basis set of independent paths to test — more thorough than statement and branch coverage.
Practise with Mutqan tools
Build decision tables from conditions and actions, generate all rule combinations and collapse impossible ones.
Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.
Convert a list of numbered requirements into traceable test cases with a requirement-to-test matrix.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.