20 min read · 9 self-checks · Updated June 2026

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.

💬
Senior Engineer Insight

The most dangerous project I ever saw had 94% statement coverage on every CI run — and shipped a benefits calculation bug that underpaid claimants for six months. The coverage tool was measuring the wrong thing, and nobody asked why. Here is what I tell every team: a coverage number is a property of your tests, not your code. One well-constructed happy-path test can drive every statement green while leaving every false branch completely dark. When a developer shows me 100% statement coverage on a payment or eligibility module, my first question is always "how many tests?" If the answer is fewer than the number of decisions in that module, they have not tested the logic — they have just visited the lines. Use statement coverage to find dead code. Use branch coverage to test decisions.

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 Industry Reality

What you actually encounter on the job
  • Coverage targets are rarely self-imposed. In most NZ teams, a 70–80% statement coverage threshold is baked into the CI pipeline by a tech lead or QA manager years earlier. Nobody remembers why that number was chosen. Testers inherit the gate and are expected to maintain it, not question it.
  • Legacy codebases punish you for missing coverage. On a brownfield codebase — think an Revenue NZ integration or an insurance quoting engine built in 2008 — reaching 80% statement coverage can require months of retrofitted unit tests. Senior testers triage: they target high-risk modules first, not a uniform percentage across the whole repo.
  • 100% is suspicious, not celebratory. When a developer reports 100% statement coverage, experienced testers ask "how?" rather than "great!". It usually means the tests were written to hit lines, not to find bugs. One carefully constructed happy-path test can paint everything green while the error-handling code is completely untested.
  • Coverage tools lie by omission in microservices. In a distributed system, your unit test coverage report looks clean — but the code paths that only activate when two services are running together don’t appear in any coverage report. Testers increasingly track integration and contract coverage separately from unit statement coverage.
  • Time pressure collapses coverage conversations. On a two-week sprint with a release Friday, the argument "we have 79% coverage" usually wins over "we should add branch coverage". The realistic move is to flag the gap in the test plan, agree on a debt item, and negotiate when it gets addressed.

5 When to Use It — and When Not To

Decision guide

✓ Use it when

  • Setting a CI quality gate — statement coverage is fast to compute and gives a clear pass/fail signal for dead or untouched code.
  • Auditing a legacy codebase: statement coverage quickly reveals which modules have zero test coverage at all, giving you a prioritised hit-list.
  • Working with a brand-new function with no tests yet — 100% statement coverage is the right first milestone before adding branch coverage.
  • Your team is moving from "no coverage at all" to "some coverage" — statement coverage is the lowest-friction entry point to a coverage culture.
  • You need a fast metric for a stakeholder report: statement coverage is universally understood and tooling support is universal (Istanbul, JaCoCo, Coverage.py).

✗ Skip it when

  • You need confidence in business logic with branching conditions — statement coverage says nothing about whether the false side of any decision was ever taken.
  • The code is safety-critical or financially sensitive (payments, tax calculations, benefits eligibility) — branch or MC/DC coverage is the appropriate standard.
  • Your team is already at 100% statement coverage and treating it as "done" — the ceiling conversation is the priority, not more statement coverage.
  • You are testing code that processes user-supplied data — equivalence partitioning and boundary value analysis find far more bugs per test written than statement coverage targets.
  • The code has complex conditional logic (&&/|| chains, nested ifs) — statement coverage can make multi-condition expressions look covered when only one combination has ever been evaluated.

Context guide

How the right level of statement coverage effort changes based on project context.

Context Priority Why
Benefits NZ benefit eligibility engine or CoverNZ compensation calculator Essential Financial and social consequences demand 100% statement coverage as the baseline gate before branch coverage is applied to every decision module. An uncovered line in an eligibility rule could silently underpay or deny a claimant.
Revenue NZ PAYE or GST calculation integration Essential Revenue NZ integrations carry compliance obligations under the Tax Administration Act 1994. Statement coverage confirms no tax-code path is entirely unexercised; pair with branch coverage for contractor and student-loan conditional logic.
Harbour Bank or Pacific Bank retail payments API High Payment processing errors are immediate and visible to customers. Statement coverage as a CI gate catches completely untested error-handling paths; RBNZ oversight increases reputational stakes if a gap reaches production.
TransitNZ (TransitNZ) digital services — licence renewals, RUC, safety ratings High Public-facing government services under the AoG digital standards require measurable test evidence. Statement coverage provides a reportable CI metric; focus uncovered-line analysis on vehicle eligibility and fee-calculation branches.
Spark or Pacific Air customer-facing web application (non-payment flows) Medium Statement coverage is useful as a CI floor (e.g. 75%) to prevent completely untested features shipping. For content-heavy pages or UI components without branching business logic, the marginal return diminishes quickly compared with exploratory or usability testing.
Internal tooling or data migration script (one-off use) Low Short-lived scripts used once for a data migration (e.g. FamiliesNZ case record normalisation) rarely justify formal coverage instrumentation. A manual dry-run on a sample dataset and peer review delivers more value per hour than setting up a coverage pipeline.

Trade-offs

What you gain and what you give up when you choose statement coverage.

Advantage Disadvantage Use instead when…
Fast to compute and universally supported — Istanbul, JaCoCo, and Coverage.py all report it out of the box with zero configuration. Gives a clear CI gate that even non-technical stakeholders understand. A single happy-path test can reach 100% while every false branch, error handler, and edge case remains completely untested. The metric creates a false sense of security on decision-heavy code. Code contains if/else, switch, or loop conditions with business consequences — use branch coverage to force both sides of every decision.
Immediately reveals completely untested modules on a legacy codebase — files at 0% coverage are obvious and easy to prioritise for remediation. Cannot detect missing code — if a required Privacy Act 2020 consent-check was simply never written, there is no statement to mark as uncovered. The metric stays silent on specification gaps. You need to verify all specified behaviours exist — use specification-based techniques (equivalence partitioning, decision tables) driven from requirements, not source code.
Low friction entry point to a coverage culture — teams moving from zero tests can set a 70% gate, see measurable progress sprint-over-sprint, and build the habit before graduating to stricter criteria. Encourages line-chasing behaviour: developers write tests designed to hit statements rather than assert correct outcomes, inflating the number while adding maintenance debt and zero fault-detection value. You want to verify that tests actually catch bugs, not just execute code — use mutation testing (Stryker, PIT) to measure whether a test suite detects injected faults.
Aggregate coverage trending over time is an early-warning signal: a module dropping from 85% to 70% across three sprints indicates new code is being written without corresponding tests. Aggregate percentages obscure risk distribution — an 80% score where the uncovered 20% is entirely error-handling and eligibility logic is far more dangerous than 80% with uniform gaps across the codebase. You are testing user-supplied data (e.g. NZBN validation, GST amounts) — use boundary value analysis and equivalence partitioning to map the input space, which statement coverage cannot do.

Enterprise reality

How Statement Coverage changes when 200–300 developers are shipping to production at NZ enterprise scale

  • Coverage measurement is automated in CI and enforced by policy — teams at CloudBooks and Harbour Bank fail the pipeline if statement coverage drops below a defined threshold (typically 80%), so no engineer manually tracks it; the gate does.
  • Compliance mandates make coverage auditable, not optional — Revenue NZ and HealthNZ systems subject to the NZISM High security baseline must demonstrate test evidence for critical paths; raw coverage reports feed into audit packages reviewed by external assessors.
  • Tooling shifts from local runners to centralised platforms — at this scale teams use SonarQube or Codecov aggregated across every repo, with coverage trending dashboards surfaced in Jira or Confluence so delivery leads can spot degradation across squads without reading individual reports.
  • Uncovered statements in shared libraries cost more than in isolated services — when a single payments library is consumed by 40 squads (as is common at Pacific Bank or KiwiFirst Bank), one untested branch that reaches production can trigger incident response across dozens of downstream teams simultaneously, making coverage gates on shared code far stricter than on leaf services.

What I would do

Professional judgment — when to reach for statement coverage, when to skip it, and what to watch for.

If…
I am joining an Revenue NZ gateway integration project that has been running for two years and the team says "we have about 70% statement coverage" — but nobody can tell me which modules are at 0%
I would…
Run the coverage report filtered by module — not by aggregate percentage — and produce a ranked list of files sorted by coverage ascending. The aggregate 70% almost always hides two or three critical modules (tax-code validation, error-handling for Revenue NZ rejections, rate-change logic) sitting at under 20%. I'd prioritise those in the next sprint before arguing about whether the overall gate should move from 70% to 80%. The risk is not in the number; it is in the distribution.
If…
A developer on a HealthNZ patient-record system shows me a CI build badge reading "100% statement coverage" and says the module is ready for sign-off
I would…
Ask one question before agreeing: "how many tests?" If the module has twelve conditional paths and the test count is three or fewer, that 100% is almost certainly a single happy-path integration test sweeping all lines in one pass. I'd open the test file, count the it() blocks, and compare that to the number of if/switch statements in the module. On a HealthNZ system subject to the Health Information Privacy Code, I would require branch coverage — both sides of every eligibility, access-control, and consent decision exercised — before the module ships. Statement coverage 100% is the starting point of that conversation, not the end.
If…
I am leading test planning for a new FamiliesNZ case-management feature and the sprint is two weeks with a Friday release — the team wants to skip statement coverage measurement entirely to save time
I would…
Agree to defer a coverage threshold conversation — but not the coverage report itself. Running Istanbul or Coverage.py costs under a minute in CI and produces the report automatically. I'd insist the report runs and gets attached to the release artefact, even if we do not gate on a percentage this sprint. The reason: on a child-welfare system, if a bug reaches production and an incident review asks "what was your statement coverage on this module at release?", "we didn't measure it" is a far worse answer than "we had 68%, and here is a screenshot." Measurement without a gate still protects you; no measurement at all is the real risk.

The bottom line: Statement coverage is a diagnostic, not a verdict — always drill into which lines are uncovered before deciding whether the percentage is acceptable, because a 75% score where the missing 25% is all error-handling paths is more dangerous than a 60% score with uniform gaps.

6 Best Practices

What experienced testers do
  • ✓ Treat statement coverage as the first gate, not the last. Enforce a minimum (e.g. 75%) in CI to catch untouched code, then require branch coverage for any module with conditional logic before it merges.
  • ✓ Scope your coverage measurement. Exclude generated code, vendor packages, migration files, and boilerplate from the report. Covering auto-generated code wastes effort and inflates your number while real logic stays untested.
  • ✓ Annotate known uncoverable lines. Some statements are truly unreachable (defensive checks for impossible states). Mark them with /* istanbul ignore next */ or equivalent so the coverage report reflects real gaps, not false misses.
  • ✓ Look at which lines are uncovered, not just the percentage. A module at 85% where the missing 15% is all error-handling and edge-case paths is far more dangerous than one at 85% where the missing code is logging.
  • ✓ Track coverage trends over time, not just snapshots. A coverage percentage dropping sprint-over-sprint on a feature module is an early warning that tests aren’t keeping pace with new code.
  • ✓ Pair statement coverage with mutation testing on critical modules. Statement coverage tells you whether lines ran; mutation testing tells you whether the tests actually caught failures. Together they give a much truer picture of test quality.
  • ✓ Never write tests that exist purely to boost the coverage number. Tests written to hit lines — rather than to assert correct behaviour — add maintenance overhead with zero fault-finding value. They make the number look good and mask real gaps.
  • ✓ Document your coverage thresholds in the test plan. Record what threshold is set, why that level was chosen, which modules are excluded, and when it will be reviewed. This prevents "we’ve always done 80%" becoming a permanent unchallenged default.
  • ✓ After reaching 100% statement coverage, immediately design branch coverage tests. Use the code you just ran through statement coverage as a prompt: for every if, switch, and loop, write a test for the path you didn’t take.

7 Common Misconceptions

❌ Myth: 100% statement coverage means the code is thoroughly tested.

Reality: Statement coverage only confirms that each line executed at least once — under some input, on some path. A single happy-path test can paint every line green while every error-handling branch, every false condition, and every edge-case behaviour remains completely unchecked. Thorough testing requires branch coverage at minimum, and specification-based techniques to find missing logic that no coverage metric can detect.

❌ Myth: High statement coverage proves there are no dead code paths.

Reality: Statement coverage can reach 100% and still leave unreachable code in the codebase — it just means your tests happened to execute every existing statement. Unreachable code (statements that genuinely cannot be reached under any input) will show as uncovered, but statement coverage cannot tell you why they are unreachable. Static analysis tools, not coverage reports, are the right tool for detecting dead code.

❌ Myth: Branch coverage is just a stricter version of statement coverage — you can skip straight to it.

Reality: Branch coverage subsumes statement coverage, meaning 100% branch coverage mathematically guarantees 100% statement coverage. But the two techniques serve different diagnostic purposes. Statement coverage tells you which code has never run at all — genuinely useful when auditing a codebase for completely untested modules. Branch coverage tells you which decisions have only ever been tested one way. Use statement coverage to find the black spots, then branch coverage to test the decisions within them.

8 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 Revenue NZ 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.

Senior engineer insight

Statement coverage showed me a number I trusted — 91% — on a KiwiSaver fee-calculation module. I shipped it. Three weeks later a customer found that our early-withdrawal penalty was silently zeroed out for anyone under 30, because that branch had never been taken in any test. The line existed; it just never ran. What changed how I think about this: I now require branch coverage on any module that contains a conditional with financial consequences, regardless of what the statement percentage says.

The most common mistake: teams set a CI gate at 80% statement coverage and then stop thinking about coverage entirely. The gate catches completely untested files — it does not catch decisions that only ever go one way. That gap is where your worst production bugs hide.

From the field

I was brought in to review test coverage on an Revenue NZ gateway integration at a Wellington services company. Their CI pipeline showed a proud 85% statement coverage badge — above the 80% threshold a tech lead had set two years earlier without documentation. When I looked at the actual report, the uncovered 15% was concentrated entirely in the error-handling paths: the code that fires when Revenue NZ returns a rejected tax code or a timeout. Those paths had never been exercised because the happy-path integration tests dominated the suite. In production, the first time Revenue NZ's staging environment went down for maintenance, the error handler threw an unhandled exception and brought the whole submission queue down with it.

The lesson: an aggregate coverage percentage across a codebase tells you almost nothing. What matters is which lines are uncovered and why. In NZ government integrations especially — Revenue NZ, Benefits NZ, TransitNZ — the error and edge-case paths are the exact ones regulators care about. Always drill into the uncovered lines, not just the headline number.

Why teams fail here

  • Treating the coverage percentage as the quality signal rather than examining which specific lines are uncovered and why — a 90% score with all error paths uncovered is far more dangerous than 70% with uniform gaps.
  • Writing tests that chase lines rather than assert behaviour — happy-path tests that sweep through every statement in one pass inflate the number while leaving every false branch and error condition completely untested.
  • Inheriting a coverage gate (e.g. 75%) without knowing why it was set, then defending that number against all pressure rather than asking whether branch coverage would be more appropriate for the module at hand.
  • Applying statement coverage uniformly across the whole codebase rather than requiring a stricter standard (branch or MC/DC coverage) on modules that contain conditional business logic — eligibility engines, tax calculators, payment processors.

Key takeaway

Statement coverage tells you which lines your tests visited — not whether your logic was tested; use it to find dead code and set a CI floor, then always move to branch coverage before signing off on any code that makes decisions.

How this has changed

The field moved. Here is how Statement Coverage evolved from its origins to current practice.

1970s

Statement coverage (line coverage) is the earliest structural coverage criterion — the simplest metric: what percentage of statements does the test suite execute? Immediately practical: coverage tools count executed lines, testers can see obvious gaps.

1980s

Commercial coverage tools for C and C++ emerge. Statement coverage reporting becomes standard in safety-critical development. The 100% statement coverage target appears in avionics and medical device standards — though later superseded by stricter criteria.

1990s

JaCoCo, Emma, and Cobertura bring statement coverage to Java. Istanbul and later V8's built-in coverage bring it to JavaScript. Coverage reports become a CI artefact in virtually every modern development environment.

2010s

Industry consensus forms that statement coverage alone is a weak quality signal — it proves a statement executed, not that the outcome was tested. Many teams achieve 80-90% statement coverage while having fundamentally inadequate tests. Branch and mutation coverage gain acceptance as better metrics.

Now

Statement coverage remains the most-reported metric because it is the easiest to explain to management. AI tools use coverage data to identify uncovered code and suggest tests. The sophisticated view: coverage is a diagnostic for finding untested areas, not a target to optimise.

Self-Check

Click each question to reveal the answer.

Interview Questions

What NZ hiring managers ask about Statement Coverage — and what strong answers look like.

Why can 100% statement coverage still leave significant bugs unfound?

Strong answer: Statement coverage only tells you that each line of code executed at least once. It does not tell you whether the outcome of executing that line was correct. A test can execute a line with a wrong calculation and still "cover" it if no assertion checks the result. It also does not ensure that the false branch of a conditional is tested — a test that always executes the true branch achieves 100% statement coverage of the true-branch code without testing the false path at all. Branch coverage is a stricter criterion that exposes this weakness.

Grad/Junior

Your manager asks why the team's 90% coverage metric did not prevent a production bug. How do you explain this?

Strong answer: Coverage metrics measure whether code was executed, not whether it was tested correctly. The 90% means we have tests that run 90% of the code — but running code is not the same as asserting on its outcomes. To use an analogy: a car inspection that checks whether all the parts are present (100% coverage) is not the same as verifying each part works correctly. I would recommend adding mutation testing to complement coverage — it reveals which tests detect injected bugs and which merely execute code without catching problems. I would also look at whether the production bug was in the 10% uncovered code or in the 90% that was covered-but-not-verified.

Junior/Mid

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.

Q6: Your team is testing the Benefits NZ benefit eligibility engine. A senior dev says "we're at 78% statement coverage — that's well above the 70% gate, so we're good to release." What would you check before agreeing?

A: First, look at which 22% of statements are uncovered — not just the percentage. If the uncovered lines are concentrated in eligibility-decision branches (the exact logic that determines whether someone receives a benefit), a 78% aggregate score masks serious risk. On a system with financial and social consequences like Benefits NZ benefit payments, the coverage threshold should apply to critical modules individually, not the whole codebase averaged together. Push for branch coverage on any module containing conditional business rules before sign-off.

Q7: What is the key difference between statement coverage and branch coverage, and why does it matter for an Revenue NZ tax-calculation module?

A: Statement coverage confirms each line ran at least once; branch coverage confirms both the true and false outcome of every decision were exercised. For an Revenue NZ tax-calculation module, this distinction is critical: a single test using a standard PAYE employee can hit every statement while never taking the false path on conditions like "is a contractor" or "has student loan deduction." Branch coverage would force separate tests for each side of those decisions, making it far more likely to expose a bug in the contractor or student-loan code paths. On tax logic, branch coverage is the minimum acceptable standard.

Q8: A developer tells you: "I wrote one integration test that exercises every line of the KiwiSaver withdrawal module — we have 100% statement coverage, so we don't need any more tests." What is wrong with this reasoning and how do you respond?

A: The developer is confusing coverage with correctness. One test that visits every line achieves 100% statement coverage but only ever tests one combination of inputs and one set of decision outcomes. The false sides of every conditional in the module — for example, a withdrawal request with zero balance, or an applicant not meeting the hardship criteria — are never exercised. A bug that incorrectly approves an ineligible withdrawal would still pass that single test. The correct response is to explain that statement coverage is a floor (no dead code), not a quality ceiling, and to propose a minimum branch coverage test set that takes both sides of every decision.

Q9: Name two scenarios where you should NOT rely on statement coverage as your primary quality signal, and explain why a different technique is more appropriate in each.

A: First, safety-critical or high-stakes financial code — such as an CoverNZ injury compensation calculator or a RealMe identity verification gate. Here, statement coverage cannot detect logic errors on untaken decision paths, so branch coverage or MC/DC coverage is required. Second, user-supplied input processing — such as a form accepting NZBN numbers or GST amounts. Statement coverage does nothing to map the input space; equivalence partitioning and boundary value analysis are far more effective at finding the invalid-input bugs that matter most. In both cases, relying on statement coverage alone creates a false sense of security while leaving the highest-risk paths untested.

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? %