Statement Coverage
Statement coverage measures how many of a program's executable statements a set of tests ran at least once. In statement testing, the white-box technique behind it, the coverage items are the executable statements, and you design tests until all of them, or an agreed share, have run.
Example: a function has 6 executable statements and your only test runs 5 of them. Statement coverage is 5 / 6 = 83%. A second test that reaches the sixth statement brings it to 100%: every statement has run at least once, which is all the criterion promises.
Why it matters
A defect can only cause a failure when the code containing it runs. Statement coverage points at code that no test has ever executed, where a defect cannot show up in those tests. It is also the simplest coverage measure to explain.
Its weakness matters as much. 100% can be reached with very few tests that leave decision outcomes, loop cases and data values untested; branch coverage, the subject of the next lesson, is a stronger criterion. A tester who knows exactly what it proves can use it to find gaps without treating it as proof of good testing.
What statement coverage measures
In the ISTQB Foundation syllabus v4.0, statement coverage is measured as the number of executable statements exercised by the tests divided by the total number of executable statements, as a percentage: statement coverage = statements executed / executable statements × 100%.
An executable statement is one that performs an action when the program runs. Each statement counts once, however many tests run it and however often a loop repeats it. At 100%, every executable statement has run at least once, so a statement containing a defect has run too, which may cause a failure that reveals it. "May" is the key word, as the worked example shows.
- Count: assignments and declarations with a value (
let fee = 5;), calls,returnandthrow, and eachif,switchor loop statement itself: evaluating its condition is an action. - Do not count when you work by hand: comments, blank lines, lines holding only a brace, a function's signature, or type-only code such as a TypeScript
interface. - A statement is covered when at least one test executed it, whether or not that test passed or asserted anything.
- Tools differ in the details: coverage.py records executed lines, and a Python
defline runs when the module is imported, so it counts as covered even if no test calls the function. Compare percentages only from the same tool, settings and files.
Worked example: a parking fee function
Requirement: "Parking is free for the first 30 minutes. After that, every started hour costs 5.00 SAR. Members pay half." The developer wrote the function and two unit tests; Math.ceil on line 4 rounds up, which implements "every started hour". Its executable statements are lines 2, 3, 4, 6, 7 and 9; the signature and the closing-brace lines are not statements.
parkingFee has 6 executable statements: T1 runs 5 of them (83%) and T2 runs all 6 (100%), yet the wrong starting fee in statement 1 never makes either test fail.parkingFee, its two unit tests and their statement coverage
1 function parkingFee(minutes, member) { 2 Part 1:let fee = 5; 3 Part 2:if (minutes > 30) { 4 Part 3:fee = Math.ceil((minutes - 30) / 60) * 5; 5 } 6 Part 4:if (member) { 7 Part 5:fee = fee / 2; 8 } 9 Part 6:return fee; 10 } Executable statements: lines 2, 3, 4, 6, 7, 9 (6 in total) Unit test Expected Actual Result Lines run Statement coverage T1 parkingFee(90, false) 5 5 passed 2 3 4 6 9 5/6 83% T2 parkingFee(200, true) 7.5 7.5 passed 2 3 4 6 7 9 6/6 100% T1 + T2 2 3 4 6 7 9 6/6 100%
- 1–6The 6 coverage items: the denominator of the formula
- 1Statement 1 · line 2Runs in T1 and T2. It holds the defect, a starting fee of 5 instead of 0, but line 4 overwrites it in both tests, so neither fails.
T1 · T2Caution - 2Statement 2 · line 3: a decisionRuns in both tests, each time with the outcome
true. Itsfalseoutcome has no statement, so statement coverage never asks for it.T1 · T2 - 3Statement 3 · line 4Runs in both tests: 90 and 200 minutes are both over 30.
T1 · T2 - 4Statement 4 · line 6: a decisionRuns in both tests:
falsein T1,truein T2.T1 · T2 - 5Statement 5 · line 7Runs only in T2, the member test. With T1 alone, coverage is 5 / 6 = 83%.
T2 - 6Statement 6 · line 9Runs in both tests.
T1 · T2
The signature and the closing-brace lines are not executable statements, so they count in neither part of the formula.
- T1
parkingFee(90, false)runs lines 2, 3, 4, 6 and 9 and returns the expected 5. Line 7 does not run becausememberis false: 5 / 6 = 83%. - T2
parkingFee(200, true)runs all six: 170 minutes after the free period are 3 started hours, 15.00 SAR, halved to 7.50. Together T1 and T2 reach 6 / 6 = 100%, and T2 alone would too. - A test from the requirement's first sentence, T3
parkingFee(20, false), expects 0 and gets 5: line 2 starts the fee at 5 instead of 0, so the free period is charged. - Line 2 ran in both T1 and T2, yet neither failed: line 4 overwrote the wrong value each time. The defect shows only when
minutes > 30is false, an outcome with no statement of its own, so statement coverage never asked for it. Branch coverage, in the next lesson, does.
Text version
1 function parkingFee(minutes, member) {
2 let fee = 5;
3 if (minutes > 30) {
4 fee = Math.ceil((minutes - 30) / 60) * 5;
5 }
6 if (member) {
7 fee = fee / 2;
8 }
9 return fee;
10 }
Executable statements: lines 2, 3, 4, 6, 7, 9 (6 in total)
Unit test Expected Actual Result Lines run Statement coverage
T1 parkingFee(90, false) 5 5 passed 2 3 4 6 9 5/6 83%
T2 parkingFee(200, true) 7.5 7.5 passed 2 3 4 6 7 9 6/6 100%
T1 + T2 2 3 4 6 7 9 6/6 100%What 100% statement coverage does not prove
The ISTQB syllabus names two limits: executing a statement does not detect every defect in it, such as a data-dependent one, and 100% statement coverage does not ensure that all the decision logic has been tested. They appear in recurring shapes:
- Missing outcomes: an
ifwithout anelsehas afalseoutcome with no statement of its own. The parking defect lives there. - Data-dependent defects:
average = total / countruns without a failure whencountis 3, and fails or returns a meaningless value when it is 0. The statement is covered either way. - Loops: one pass through a simple loop body covers every statement in it. Zero passes (an empty list) and many passes are never required.
- Compound conditions:
if (gold || total >= 200)is one statement, covered by a test wheregoldis true, and in most languages the second condition is then not even evaluated. The lesson on condition coverage returns to this.
Using statement coverage well
Statement coverage is most useful as a floor: a statement that no test runs is a certain blind spot. It is least useful as a goal, because the cheapest way to raise it is a test that runs code and checks little.
- Start from requirement-based tests (equivalence partitioning, boundary value analysis), measure, and only then add tests for statements that never ran. Partitioning the parking minutes into "up to 30" and "over 30" produces T3, which no coverage report asked for.
- When a statement cannot be reached in a test environment, teams can exclude it explicitly, as with coverage.py's
# pragma: no coveror Istanbul's/* istanbul ignore next */. Review every exclusion: excluded code is no longer measured at all.
Try it: Equivalence Partition Generator Try it: Boundary Value Generator
Common mistakes
- Counting comments, braces or the function signature as statements, which changes the denominator.
- Reading 100% statement coverage as "every path tested". It means every statement ran once, perhaps with harmless data and the same decision outcomes.
- Writing a test only to execute an uncovered statement, with no assertion or with an expected result copied from the code.
- Counting a statement twice because two tests ran it or a loop repeated it: the numerator counts distinct statements.
- Excluding hard-to-reach code without review, so the percentage rises while the risk stays.
The QA perspective
A tester rarely calculates statement coverage by hand at work; the tool does. Doing it by hand a few times teaches you what the number means, so you read reports critically: which statements never ran, which decision guards them, and whether a requirement describes them. When a developer reports "100% statement coverage", ask which tests produced it, what they assert, and which decision outcomes and data cases they never exercised.
On the team, agree which criterion a coverage target uses and which code it applies to, record it in the test plan or the pipeline's quality gate, and report it with its scope: "unit tests, statement coverage of the fee module: 100%; the false outcome of the free-period check has no test". For risky logic, ask for branch coverage as well.
Practice exercise
Measure the statement coverage of a rating function
Requirement: "A product page shows the average customer rating with one decimal place. Products with an average of 4.5 or more show Top rated instead. Products without ratings show No ratings yet." Below are the function and its only unit test; toFixed(1) turns a number into text with one decimal place.
Count the executable statements, calculate the statement coverage of T1, add one test that brings the set to 100%, and check what the requirement still leaves untested.
1 function averageRating(ratings) {
2 let sum = 0;
3 for (const r of ratings) {
4 sum = sum + r;
5 }
6 const avg = sum / ratings.length;
7 if (avg >= 4.5) {
8 return 'Top rated';
9 }
10 return avg.toFixed(1);
11 }
Unit test Expected Actual Result
T1 averageRating([5, 4]) 'Top rated' 'Top rated' passed- List the executable statements by line number.
- Mark the statements T1 runs and calculate its coverage.
- Add one test so that T1 and the new test together reach 100%, with the expected result from the requirement.
- Find the requirement sentence no test covers, trace it through the code and decide whether it passes.
Show the model answer
Executable statements: lines 2, 3, 4, 6, 7, 8 and 10, seven in total.
T1 (sum 9, average 4.5) returns on line 8. It runs lines 2, 3, 4, 6, 7 and 8 but not line 10: 6 / 7 = 86%.
Add T2 averageRating([3, 4]): average 3.5, expected 3.5. It runs line 10, so T1 and T2 reach 7 / 7 = 100%. No single test can: lines 8 and 10 are alternative return statements.
No test covers "Products without ratings show No ratings yet". Trace averageRating([]): the loop body runs zero times, sum / ratings.length is 0 / 0, which is NaN (Not a Number) in JavaScript, NaN >= 4.5 is false, and line 10 returns the string 'NaN'. Expected No ratings yet, actual 'NaN': the test fails. The rule was never implemented, and line 6, covered by both tests, goes wrong only with this data.
At 100% statement coverage, the empty list (zero loop passes), the division by zero and a missing requirement were all still untested. A question for the product owner too: an average of 4.46 is displayed as 4.5 without the badge. Which value should the rule use?
Frequently asked questions
What is a good statement coverage percentage?
There is no universal number, and the ISTQB syllabus does not set one. Teams set a threshold by risk and apply it to the code that matters, often to changed code. A high total with critical statements uncovered is worse than a lower one where the uncovered code is known and accepted.
How many tests do I need for 100% statement coverage?
As many as it takes to reach every statement at least once, which is often far fewer than the requirement needs: in the worked example one test, T2, was enough. The number of tests is a consequence, not a target. Design tests from the requirement first and use coverage to find what they missed.
Does 100% branch coverage give 100% statement coverage?
Yes. The ISTQB syllabus states that 100% branch coverage achieves 100% statement coverage, but not the other way round: the parking example reaches 100% statement coverage without ever taking the false outcome of minutes > 30. The next lesson explains why.
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.
- 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
Derive valid and invalid equivalence classes for each input and pick representative test values.
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.