ProductHow it worksPricingBlogDocsLoginFind Your First Bug
White box testing coverage as four concentric rings around a branching code structure, with unlit crescent tiles stranded outside every ring
TestingWhite Box TestingCode Coverage Criteria+1

What Is White Box Testing? 4 Coverage Criteria, Counted

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma

White box testing is testing performed with access to the implementation, not just the interface, which lets you choose inputs that exercise specific internal paths and measure exactly which structures ran. It is defined by four coverage criteria: statement, branch, condition, and path. On the twenty-three-line function counted below, they cost four, four, six, and eight test cases, because path coverage multiplies decisions where the others only add them.

Somewhere in an interview, or a study guide, or a wiki page someone asked you to update, you got asked to name the white box coverage criteria. Statement, branch, path, maybe condition if you had read the right chapter recently. Then someone asked how many test cases branch coverage demands on a real function, and the four words stopped being useful. That gap, four vocabulary words with no arithmetic behind them, is the subject here. Not a QA lead deciding how much of a suite should be structural versus behavioral, and not someone working out whether their AI-generated tests test anything real. Just the person who has to say what these words mean and back it with a number.

What white box testing actually grants you

White box testing is testing conducted with access to the implementation. Not the requirements document, not the UI, the actual source: the branches, the conditionals, the loops, the internal state a black box test can never see directly.

A black box test picks inputs based on what the interface promises and checks outputs against that promise. A white box test can pick inputs specifically because they walk through a nested branch nobody else would think to hit, assert against internal state that never reaches the interface, flag code no input can reach at all, and measure precisely which structures a given suite actually exercised.

Black box and white box were never descriptions of a system. They described what a person doing the testing was allowed to look at while they worked. A tester with a login and a browser tab does black box testing regardless of how the system is built. A tester with the source, the same login, does white box testing on that same request, because now they can aim at the discount branch nobody without the code would ever find. The full three-way version, including where grey box testing sits between the two, is its own piece: the black box versus white box comparison.

This access has one real purpose: it makes coverage measurable at all. If you cannot see the branches, you cannot count them. Autonoma also reads the codebase before deciding what to test, for a different reason than counting branches, worth returning to once the arithmetic below is on the table.

The four white box testing coverage criteria, counted

Here is the function all of the counting below is done against, written for this post, not lifted from a textbook: a discount-rate calculation, twenty-three lines, two independent decision trees and a trailing override, two compound conditions.

function calculateDiscountRate(customer, order) {
  let rate;

  if (customer.isNewCustomer) {
    if (order.totalCents >= 10000 || order.itemCount >= 5) {
      rate = 0.10;
    } else {
      rate = 0.05;
    }
  } else {
    if (customer.loyaltyTier === "gold" && order.totalCents >= 5000) {
      rate = 0.20;
    } else {
      rate = 0.08;
    }
  }

  if (order.isFinalSaleItem) {
    rate = 0;
  }

  return rate;
}

module.exports = { calculateDiscountRate };

calculateDiscountRate(customer, order) works like this. New customers get a nested decision on whether the order clears a size-or-item-count bar. Returning customers get a different nested decision on whether they hold gold loyalty status and clear a total-spend bar. Either way, a final, independent check zeroes the rate out if the order is a final-sale item.

Statement coverage: every executable line runs (4 tests)

Statement coverage asks whether a suite executed every line capable of executing. Four assignment statements here can each only run under one specific combination of decisions: the new-customer, big-order branch; the new-customer, small-order branch; the returning-customer, gold-tier branch; the returning-customer, non-gold branch. Each needs its own test, and none of the other three will reach it. That is four tests minimum. One of those four can also be the test where the order is flagged as a final sale, so the fifth assignment, the override to zero, rides along free. Statement coverage on this function costs four tests.

Branch coverage: every decision both ways (4 tests)

Branch coverage asks whether both the true and false outcome of every decision were exercised, not just whether a line ran. Four decisions live here: is the customer new, does the order clear the size-or-count bar, does the returning customer clear the gold-tier-and-total bar, and is this a final sale item. That is eight branch outcomes. The same four tests that gave full statement coverage already hit all eight, because the four leaf assignments sit on opposite sides of every decision. That is not free on every function; it holds here because the tree has no dead branches. The moment a function has a combination nothing can reach, branch and statement coverage start to diverge.

Condition coverage: every atomic condition both ways (6 tests)

Condition coverage is where the arithmetic breaks from branch coverage, because two of the four decisions are compound: the new-customer check is an OR (order total above one threshold, or item count above another), the returning-customer check is an AND (gold tier, and order total above a different threshold). Branch coverage only cares that the OR or the AND evaluated true once and false once, overall. Condition coverage cares whether each half of that OR and that AND independently took both values on its own. Because both short-circuit, satisfying the branch outcome does not guarantee the second half was ever evaluated. Across the four statement tests, the first half of the OR and the AND each get exercised both ways, but the second half of each only ever gets one value, because the first half already decided the outcome. Closing that gap costs two more tests, one per compound condition's hidden half. Condition coverage on this function costs six tests.

Two four-row truth tables for the new-customer OR condition, order total above a threshold or item count above a threshold, showing the two rows branch coverage's four tests actually exercise and the additional row, order total false and item count true, that condition coverage needs to make the OR's second half ever evaluate true
Branch coverage leaves the OR's second half stuck on one value. The AND's hidden half needs the same fix, for six tests total.

Path coverage: every distinct route through the function (8 tests)

Path coverage asks for every distinct route through the function, not every decision counted in isolation. The new-versus-returning split contributes two routes, each contributing two more depending on its nested check, for four routes through that half. The final-sale check runs afterward regardless of which route was taken, doubling the count again: four routes times two outcomes is eight distinct paths. That is the arithmetic path coverage runs on, multiply the independent choices, do not add them. Four decisions arranged this way need eight tests, on twenty-three lines with no loop at all. Add a loop over a cart's line items with a conditional per item, and the count stops being fixed: it becomes a function of cart size, why path coverage is rarely pursued past a handful of decisions. All eight of those cases, one per distinct path with the expected rate for each, are runnable as-is in src/path-coverage-cases.js.

The calculateDiscountRate control-flow graph in three passes: statement coverage reaching all five rate assignments in four tests, branch coverage exercising all eight outcomes of its four decisions in those same four tests, and path coverage unrolling the final-sale check per route to give eight distinct routes from entry to return
Statement and branch coverage land on the same four tests here. Path coverage multiplies the same four decisions out to eight.

Four tests, four tests, six tests, eight tests. Statement and branch coverage landed on the same number here because the decision tree has no dead ends; that will not hold on every function. Boundary value analysis decides which specific numbers go into each test once you know how many you need.

The blind spot: structural coverage cannot find what was never built

Every count above assumes the code being measured is the code that should exist. Structural coverage has no way to check that assumption, because it only ever measures what already runs. If the discount function is missing an entire rule, say a mandated regional discount for a jurisdiction the business only entered last quarter, there is no line, branch, condition, or path for that rule to occupy. A suite can sit at 100% path coverage here and still be blind to the fact that a required fifth branch was never written.

Two overlapping circles, code that exists and behavior that was required, with the non-overlapping crescent of the required-behavior circle shaded and labeled structurally invisible, because a rule that was never coded has no line, branch, condition or path for coverage to count
Coverage measures the overlap. It has no instrument that can see the crescent.

That is not a hole in the technique. It is the definition of the technique: white box testing evaluates the code against itself, not against a requirement that lives somewhere else, in a ticket or a spec nobody linked back to the function.

The more common version of the gap does not even require a missing branch. Suppose the gold-tier discount should require twelve months of tenure, and the engineer implemented the gold-tier check without the tenure clause. Every one of the eight paths above still executes exactly as written. Statement, branch, condition, and path coverage all read 100%, because the wrong rule still has a branch, a condition, and a path, it is just the wrong one. Nothing about the coverage number can tell you the rule inside it is wrong, a pattern that shows up constantly in AI-generated test suites optimized for coverage rather than correctness.

White box testing, in other words, was built for an older constraint: running every test case used to cost real machine time, so picking a small, structurally justified set of cases mattered. That constraint mostly disappeared once continuous integration made running a full suite cheap. What survives, and what the four criteria never touched, is the part of testing that checks whether the answer a test expects is the correct answer, not whether the code path that produced it ran. Mutation testing gets closer by checking whether your assertions would catch a deliberately introduced bug, and assertion coverage gets there from a different angle by checking whether your tests assert anything meaningful at all. Neither is white box testing. Both exist because white box testing, by definition, stops at the code.

How Autonoma reads code without testing it structurally

The blind spot above is a structural fact about white box testing, not a criticism of the people who use it well. It just means structural coverage answers a narrower question than most people assume when they quote a coverage number: it tells you what ran, never what should have run, and it cannot see a requirement that has no code behind it at all.

Autonoma's agents read the codebase the same way a white box tester does, before a single test executes, to work out what the application's routes, components, and user flows actually are. But the coverage question our agents answer afterward is behavioral rather than structural: they verify by driving the running application in a live preview environment and checking that the observable behavior matches what the code implies it should do, not by counting which statements or branches got exercised along the way. A missing rule shows up here the same way it would to a human reviewer looking at the running app, not as a percentage that stays silent because there was never a line to cover in the first place.

This is architecture, not a coverage number to set against the arithmetic above; behavioral verification and the statement, branch, condition, and path counts worked through above are different exercises answering different questions. Grey box testing sits between the two worlds, using enough source access to plan intelligently without treating every internal branch as the target, and our grey box testing breakdown covers that construction in full.

Where white box testing belongs in a suite

White box testing lives at the unit level, and the tooling for it is mature and unglamorous: coverage instrumentation built into Jest for JavaScript and TypeScript, pytest-cov for Python, JaCoCo for Java. These tools compile the statement, branch, and sometimes condition coverage numbers derived by hand above, automatically, on every test run, against your actual codebase rather than a twenty-three-line example.

Two stacked bands showing the unit layer, with Jest, pytest-cov, and JaCoCo boxes for statement, branch, and path coverage, above the end-to-end layer for behavioral verification, connected by an arrow labeled neither replaces the other
The unit layer counts structural coverage. The end-to-end layer checks whether the behavior it produces is correct.

Autonoma does not operate at this layer: unit-level structural coverage is not something our agents produce or replace, and Jest, pytest-cov, and JaCoCo remain the right tools for it. A suite that only has one of these two layers is missing information the other supplies, and a mature test strategy budgets for both rather than picking one and calling it complete. Test design techniques covers the other six approaches that sit alongside white box testing in that budget, and the software testing terminology guide is the place to see where all of these words sit relative to each other.

A rough allocation that holds up in practice: pure functions with meaningful branching, like the discount calculation above, belong at the unit level, where a coverage tool can enumerate every path in milliseconds. Anything that only manifests once a browser renders it, a network call resolves, or two services talk to each other belongs at the end-to-end layer, because no amount of statement or branch coverage on the individual functions involved tells you whether they compose correctly once wired together. Teams that skip the unit layer end up writing slow, flaky end-to-end tests to check arithmetic a coverage tool could have verified in milliseconds. Teams that skip the end-to-end layer end up with a green coverage report and an outage in a flow their coverage tool never modeled in the first place.

Picture a CI pipeline that gates merges on a single branch coverage threshold across the whole repository, configured through something like Jest's coverageThreshold option, say 80%, purely as an illustration. That number, whatever a given team sets it to, is a floor against the worst outcome, code nobody ever ran a test against, not a ceiling on quality. A pull request can clear an 80% gate like that while the one function it changed sits at 100% on every structural metric counted above and still ships the wrong gold-tier rule from the earlier example, because the gate only asks whether lines and branches executed, never whether the values inside them are the correct values. Treat a threshold like that as a hygiene check, the same way a linter is a hygiene check: necessary, cheap to enforce, and answering a much narrower question than "does this code do the right thing."

Four words, four arithmetic answers: four tests for statement coverage, four for branch, six for condition, eight for path, all on a function with twenty-three lines and one loop away from making that last number meaningless. That is what to say the next time someone asks you to name the criteria. Not just the words, the cost of each one, and the reason two of the four numbers matched here while the other two did not.

The interview version of this answer is short: white box testing is defined by access, not by who holds the job title, and the four criteria are an ordering of how expensive each one is to satisfy, not four interchangeable synonyms for "we have tests." The harder, more useful version is the one worked out above at length: those four numbers describe what ran, in what combination, and nothing in any one of them describes whether what ran was correct, which is a separate question a coverage report was never built to answer.

None of that arithmetic tells you whether the rule inside the function is the right rule, and that gap is exactly where Autonoma picks up. Keep the unit-level coverage tools for the question they answer well. Bring in something that verifies behavior for the question they cannot.

Frequently Asked Questions

White box testing is testing performed with access to the implementation rather than just the interface. Instead of picking inputs based only on what a system's documentation or UI promises, a white box tester can choose inputs that specifically route through a particular branch, assert against internal state, and measure which parts of the code a given test suite actually exercised.

The four standard structural coverage criteria are statement coverage, which counts whether every executable line ran; branch coverage, which counts whether every true and false outcome of every decision ran; condition coverage, which counts whether every individual atomic condition inside a compound decision independently took both values; and path coverage, which counts whether every distinct route through the function's decisions ran. Each criterion is strictly harder to satisfy than the one before it, and the number of tests required tends to grow with each one, sometimes sharply.

Anyone with source access can perform white box testing, and in practice it is usually developers writing unit tests against their own code, since they already have the implementation open. The defining factor is not job title, it is whether the code is open in front of the person choosing what to test. A QA engineer with repository access doing the same thing is also doing white box testing.

The difference is access, not role. White box testing is done with access to the implementation, so test inputs and assertions can target specific branches, conditions, and internal state. Black box testing is done using only the interface, the inputs and outputs a system exposes, with no reference to how the code inside produces them. The same person can do both on the same system depending on what they choose to look at while designing a given test. A full three-way comparison against grey box testing lives in our dedicated black box versus white box guide.

No. White box testing measures the code that exists against itself, checking whether a given test suite executed a certain statement, branch, condition, or path. If a requirement was never implemented, there is no code for it, and therefore nothing for statement, branch, condition, or path coverage to count. A suite can report 100% coverage on every metric and still miss an entire missing feature, because coverage only ever describes what was built, never what should have been built.

Autonoma answers a different coverage question, one built to complement statement, branch, condition, and path coverage rather than replace them. Our agents read a codebase to plan end-to-end tests, then run them against a live preview environment to check something structural coverage tools like Jest, pytest-cov, or JaCoCo can't: whether the running application actually does what the code implies it should do, not just which lines or branches executed along the way. Keep those tools for coverage at the unit level; bring in Autonoma for the behavioral layer they were never built to check.

Related articles

Six test automation anti-pattern icons, including an inverted ice cream cone test pyramid, arranged around a single root-cause symbol

Test Automation Anti-Patterns: 6 Failures, 1 Root Cause

Six test automation anti-patterns, each with a diagnostic tell you can check today, and the one root cause behind every one of them.

Grey box testing shown as three cubes, white, grey and black, with a source code sheet above the grey cube and a browser window below it, both linked to the same point on its face

Why Grey Box Testing Was Never a Compromise

Grey box testing was a compromise because one person could only hold one perspective at a time. Here's what changes when a system holds both perspectives fully.

A horizontal agent trajectory diagram showing a tool call passing a right-tool checkpoint but failing an argument-accuracy checkpoint

How to Test AI Agents That Take Actions (Tool Calls)

A runnable guide to testing tool-calling agents: right tool, right order, right arguments, mocked vs live calls, failure handling, and non-determinism.

A chatbot test pipeline moving from manual QA through scripted and semantic assertions into an automated CI gate that samples the model N times before allowing a merge

Chatbot Automation Testing: Why Assertions Fail

Chatbot automation testing that survives non-deterministic replies: the migration to a CI gate, n-run sampling, threshold gating, and real GitHub Actions YAML.