White Box · Structure-Based

Statement Coverage

Execute every executable statement in the code at least once. It’s the most basic white box metric — a necessary starting point, not a sufficient end point.

Senior ISTQB CTFL v4.0 — 4.3.1

1 The Hook

A Wellington fintech ships a payments module with a proud number on the dashboard: 100% statement coverage. Every line runs in the test suite. The team treats that as a green light and pushes to production.

Two days later, refunds start failing for non-members. The bug sat in a refund-fee path that only fires when isMember is false. The single test that drove coverage to 100% used a member account with a large order — it ran every line, but it never once took the false side of the membership check. The fee logic on the false branch was wrong, and no test ever went there.

That is the trap with statement coverage. Running a line is not the same as testing the decision that leads to it. A solitary test can sweep through every statement while leaving half the logic unexercised — and a 100% number on the report quietly hides it.

2 The Rule

Statement coverage tells you only whether each line was run at least once — never whether the logic was tested. Treat 100% as a floor (no dead code left unrun), not a ceiling (proof of correctness), and always move up to branch coverage for real logic.

3 The Analogy

Analogy

Walking every street in your suburb — once, in one direction.

Imagine a courier who claims to know Island Bay because they have driven down every street at least once. Technically true — every road has been covered. But they only ever drove each street one way, in dry weather, at midday. They have never reversed out of the dead-end, never met the school-zone traffic, never taken the left turn instead of the right at the roundabout. They have been everywhere without having handled everywhere.

Statement coverage is that courier. It proves every line was visited. It says nothing about whether you took each decision both ways, in both directions — that is what branch coverage adds.

What it is

Statement coverage (also called line coverage) measures whether each executable statement in the code has been executed at least once during testing. It’s expressed as a percentage: statements executed ÷ total statements × 100.

It answers the question: is there any code we haven’t run at all? It doesn’t answer: have we tested all the logic?

Measuring it

Statement coverage is measured by code coverage tools (Istanbul/nyc for JavaScript, JaCoCo for Java, Coverage.py for Python, etc.). These tools instrument the code and report which lines were hit during your test suite.

Worked example

Consider this function that calculates a discount:

Code with statement coverage analysis
function getDiscount(user, total) {
  let discount = 0;                    // S1 — always executed
  if (user.isMember) {                 // S2 — decision point
    discount = 0.10;                   // S3 — only if member
    if (total > 100) {                 // S4 — only if member
      discount = 0.15;                 // S5 — only if member AND total > 100
    }
  }
  return discount;                     // S6 — always executed
}
Coverage achieved by test input
Test inputStatements hitCoverage
Non-member, total = 50S1, S2, S650%
Member, total = 50S1, S2, S3, S4, S683%
Member, total = 150S1, S2, S3, S4, S5, S6100%

Notice: a single test with member, total = 150 achieves 100% statement coverage. But it doesn’t test the non-member case at all.

The limits of statement coverage

  • 100% statement coverage doesn’t mean all logic is tested. A single test that executes all statements may never test the false branch of any decision.
  • It doesn’t find missing code. If a required validation was simply never written, it won’t show up as uncovered.
  • It doesn’t test combinations. Each statement is hit once — not every combination of conditions.

Coverage is a floor, not a ceiling. 100% statement coverage is a minimum bar, not proof the code is correct. Teams that treat it as a quality target are measuring the wrong thing.

ISTQB mapping

ISTQB CTFL v4.0 reference
RefTopic
4.3.1Statement Testing and Coverage
FL-4.3.1 K2Explain statement testing and statement coverage
FL-4.3.1 K2Explain the reasons for measuring statement coverage

4 Now You Try

Three graded exercises — spot, fix, then build. Write your answer, run it for AI feedback, then compare to the model answer.

🔍 Exercise 1 of 3 — Spot: count the statement coverage

A NZ Super payment function has 6 executable statements. One test runs with age = 70, isResident = true:

function calcSuper(age, isResident) {
  let amount = 0;                  // S1
  if (age >= 65) {                 // S2
    amount = 463;                  // S3
    if (isResident) {              // S4
      amount = amount + 50;        // S5
    }
  }
  return amount;                   // S6
}

List which statements that one test hits, give the coverage percentage, and name which statement is never run.

Show model answer
Test (age=70, isResident=true) hits: S1, S2, S3, S4, S5, S6 — all six.
Coverage: 100% (6/6).
Statement never run by this test: none — this single test happens to hit every statement.

Why it is still weak: 100% statement coverage here does NOT mean the logic is tested. The false side of S2 (age < 65) and the false side of S4 (not a resident) are never exercised. If the code wrongly paid Super to someone under 65, this test would still report 100% coverage and pass. Statement coverage counts lines run, not decisions tested.
🔧 Exercise 2 of 3 — Fix: repair a flawed coverage claim

A tester reports the result below for an IRD tax-code validator. The claim is wrong: the percentage is miscalculated and the conclusion overstates what statement coverage proves. Rewrite it correctly.

Flawed claim:
Function has 8 statements. Our 2 tests hit S1, S2, S3, S5, S6, S8 — that's 6 lines.
"Coverage = 6/8 = 90%. Close enough to 100%, so the logic is fully tested. Ship it."

Rewrite the claim correctly:

Show model answer
Correct coverage: 6 of 8 = 75%, not 90%. (6 ÷ 8 = 0.75.)
Uncovered statements: S4 and S7 were never hit — those lines have not been run at all.

What is wrong with the conclusion:
- The arithmetic is wrong: 6/8 is 75%, not 90%.
- Even at 100%, statement coverage would not prove "the logic is fully tested." It only proves every line ran at least once. It says nothing about whether each decision was taken both ways, whether combinations were tried, or whether required code was even written.
- "Close enough, ship it" is the classic mistake: coverage is a floor, not a quality target. With S4 and S7 unrun, there is real untested code, and the next step is branch coverage, not shipping.
🏗️ Exercise 3 of 3 — Build: design a minimum test set

A KiwiSaver hardship-withdrawal checker has the statements below. Design the smallest set of test inputs that achieves 100% statement coverage, and state how many tests you need.

function canWithdraw(balance, inHardship) {
  let result = 'declined';         // S1
  if (balance > 0) {               // S2
    if (inHardship) {              // S3
      result = 'approved';         // S4
    }
  }
  return result;                   // S5
}
Show model answer
Minimum for 100% statement coverage: ONE test.
- Test 1: balance = 100, inHardship = true → hits S1, S2, S3, S4, S5 — all five statements.

A single test reaches every statement because the only statement past the two ifs (S4) is reachable when both conditions are true, and S1/S2/S3/S5 are on that same path.

What statement coverage still leaves untested:
- The false side of S2 (balance ≤ 0) is never taken.
- The false side of S3 (not in hardship) is never taken.
- A bug that approves a withdrawal with a zero balance, or approves someone not in hardship, would not be caught. That is exactly why you move to branch coverage — it would force at least three tests to take both sides of both decisions.

Self-Check

Click each question to reveal the answer.

Q1: What exactly does statement coverage measure, and what does it not measure?

It measures whether each executable statement (line) ran at least once during the tests — statements executed ÷ total statements. It does not measure whether each decision was taken both ways, whether condition combinations were tried, or whether required code was even written.

Q2: How can a single test achieve 100% statement coverage yet still miss serious bugs?

If one input drives execution down a path that touches every line, every statement is "covered" — but the false sides of the decisions on that path are never taken. A bug living on a false branch (e.g. wrong behaviour for a non-member) runs zero times, so the 100% number hides it.

Q3: Why is statement coverage described as "a floor, not a ceiling"?

Less than 100% means there is code your tests never run at all — a genuine gap, so 100% is a sensible minimum bar (floor). But reaching 100% does not prove correctness, so it must never be treated as the quality target (ceiling). Teams that chase the number alone are measuring the wrong thing.

Q4: Does statement coverage detect missing code — a validation that was simply never written?

No. Statement coverage can only report on statements that exist. If a required check was never coded, there is no line to mark as uncovered, so the metric stays silent. Specification-based techniques and reviews are needed to catch missing logic.

Q5: What is the next coverage criterion to move up to, and why is it stronger?

Branch (decision) coverage. It requires both the true and false outcome of every decision to be exercised, so it forces tests onto the paths statement coverage can skip. Branch coverage subsumes statement coverage — achieving 100% branch coverage guarantees 100% statement coverage, but not the reverse.

Branch coverage is stronger — it requires both the true AND false outcomes of every decision to be exercised. Statement coverage subsumes statement execution; branch coverage subsumes statement coverage. For most production code, branch coverage is the minimum target.

Practice this technique: Try Test Lead Practice 07 — Test coverage gaps.

Try It — Calculate statement coverage

A NZ GST validation function has 7 executable statements (S1–S7). A test suite runs three tests. Work out which statements each test hits, then calculate coverage.

Function: validateGST(amount, isGSTRegistered)
function validateGST(amount, isGSTRegistered) {
  if (amount <= 0) {                  // S1
    return 'Invalid amount';           // S2
  }
  let gst = 0;                         // S3
  if (isGSTRegistered) {              // S4
    gst = amount * 0.15;              // S5
  }
  let total = amount + gst;           // S6
  return total;                        // S7
}
TestamountisGSTRegisteredStatements hit% coverage
Test 1-5false
Test 2100false
Test 3200true

After all 3 tests, what is the combined statement coverage? %