What Does Code Coverage Measure? A Lab With One Function
Code coverage explained by testing one small function run by run: statement, branch and condition coverage worked out with formulas, the test coverage formula, and the bug that survives 100% coverage.

Last updated 2026-11-01 · 10 min read · By SoftwareTestPilot
Ask five testers what code coverage measures and you'll hear "how well the code is tested" more than once. That answer is wrong in an important way. This article is set up like a small lab: one function, tests added one at a time, and the coverage numbers recalculated after every run. You'll see exactly what the percentage counts, and then a bug that stays hidden at 100%.
The short answer
Code coverage measures which parts of the code were executed while the tests ran, expressed as a percentage. A coverage tool watches the program during the test run and records every line, branch and function that ran.
What it does not measure: whether the results were checked, whether they were right, or whether the requirement was understood correctly. A test with no assertions at all still raises coverage.
The lab: one function, six lines
Here is the code under test. The business rule is in the comment at the top.
// Rule: free shipping for orders of ₹500 or more, or for members.
function shippingFee(subtotal, isMember) {
let fee = 50; // line 1
if (subtotal > 500 || isMember) { // line 2 (decision D1)
fee = 0; // line 3
}
if (subtotal < 0) { // line 4 (decision D2)
throw new Error("Invalid subtotal"); // line 5
}
return fee; // line 6
}There are 6 executable lines and 2 decisions. Each decision can go true or false, so there are 4 branch outcomes in total. Look carefully at line 2 before reading on. There's a bug in it.
Adding tests one at a time
We start with one obvious test and add more, recalculating after each run. The numbers are cumulative.
| Run | New test | Lines run (total so far) | Statement coverage | Branch outcomes hit (of 4) | Branch coverage |
|---|---|---|---|---|---|
| 1 | shippingFee(1000, false) → 0 | 1, 2, 3, 4, 6 | 5/6 = 83.3% | D1 true, D2 false | 2/4 = 50% |
| 2 | shippingFee(100, false) → 50 | 1, 2, 3, 4, 6 | 5/6 = 83.3% | + D1 false | 3/4 = 75% |
| 3 | shippingFee(-10, false) → throws | all 6 | 6/6 = 100% | + D2 true | 4/4 = 100% |
Two things to notice:
- Run 2 added no statement coverage but raised branch coverage. It took the "false" path of D1, which has no line of its own. Line coverage can't see that path; branch coverage can. That's why branch coverage is the number worth reporting.
- After run 3, both numbers are 100%. A coverage report would show the whole function in green.
The coverage types and their formulas
Every coverage type uses the same idea: items exercised ÷ total items × 100. Only the "item" changes.
| Coverage type | What it counts | Formula | Our result after 3 runs |
|---|---|---|---|
| Statement (line) | Executable statements that ran | statements run ÷ total statements × 100 | 100% |
| Branch (decision) | True and false outcomes of each decision | outcomes hit ÷ total outcomes × 100 | 100% |
| Condition | True and false of each individual condition inside a decision | condition outcomes hit ÷ total condition outcomes × 100 | 75% (isMember never true) |
| Function | Functions called at least once | functions called ÷ total functions × 100 | 100% |
Why is condition coverage only 75%? Decision D1 has two conditions: subtotal > 500 and isMember. The first was true (run 1) and false (runs 2 and 3). But isMember was false in every test, so its "true" outcome was never seen: 3 of 4 condition outcomes. Adding shippingFee(100, true) makes it 100%.
There's a trap here too. In JavaScript, Java and Python, a || b stops as soon as a is true, so in run 1 isMember was never even evaluated. Some tools report this, some don't, so check what your tool actually counts. Our white box testing techniques guide covers path and condition coverage in more depth.
The bug that survived 100% coverage
The rule says free shipping for orders of ₹500 or more. Line 2 says subtotal > 500. A shopper with exactly ₹500 in the cart pays ₹50 shipping. That's wrong, and our "fully covered" function has had this bug the whole time.
No test used ₹500, so no test could fail. Coverage counted line 2 as covered because it ran, not because its comparison was right. The test that catches it comes from the requirement, not from the coverage report:
expect(shippingFee(500, false)).toBe(0); // fails: returns 50This is a classic boundary bug, and boundary value analysis is the technique that finds it (see our functional testing walkthrough, where the same off-by-one bug shows up in a coupon). If you want a metric that does notice weak tests, look at mutation testing. It deliberately changes > to >= and similar, then checks whether any test fails.
Code coverage vs test coverage (and the test coverage formula)
The two names sound alike but measure different things:
- Code coverage comes from a tool and is measured against the code: lines, branches, functions executed.
- Test coverage is usually measured against the requirements or test conditions: how many of them have at least one test.
Test coverage formula: requirements covered by at least one test ÷ total requirements × 100. If a feature has 12 requirements and 9 have tests, test coverage is 9 ÷ 12 × 100 = 75%. A requirement traceability matrix is the usual place to read that number from.
In our lab, the requirement "₹500 or more is free" had no test, so requirement-based coverage would have flagged the gap that 100% code coverage missed. You want both numbers.
How to measure code coverage in practice
You don't count lines by hand. Common tools by language:
- JavaScript/TypeScript: Jest has coverage built in (
npx jest --coverage), using Istanbul under the hood. Vitest offers a--coverageoption as well. - Python: coverage.py. Run
coverage run --branch -m pytest, thencoverage report. The--branchflag turns on branch measurement, which is off by default. - Java: JaCoCo, usually through its Maven or Gradle plugin, which reports line and branch coverage.
Read the report for red lines and partial branches, not just the headline percentage. Each one is a concrete question: "is it OK that nothing tests this?"
What is a good code coverage percentage?
There is no universal number. One widely quoted reference is Google's own testing blog post on code coverage best practices, which describes 60% as acceptable, 75% as commendable and 90% as exemplary, and warns against treating any target as a goal in itself.
A practical approach many teams use:
- Look at coverage of changed code in each pull request, not just the whole-project total.
- Expect higher coverage on business rules (prices, permissions, money) than on glue code.
- Never accept a test written only to turn a line green. If it has no meaningful assertion, it adds risk by giving false comfort.
Answering coverage questions in an interview
A strong answer is three sentences: "Code coverage measures what share of the code ran during tests, usually as statement or branch coverage. Branch coverage is stricter because it counts both outcomes of each decision. But it doesn't prove correctness: I've seen 100% coverage miss a > vs >= boundary bug, which is why I pair it with boundary tests and requirement coverage."
If you're an aspiring SDET, practise by running coverage on a small project of your own and talking through one gap you found. You can also discuss coverage targets with other testers in the QA Community.
Key takeaways
- 1Code coverage = the share of code executed by your tests, not the share that is proven correct.
- 2Statement coverage counts lines; branch coverage counts true and false outcomes of every decision. Branch coverage is the stricter, more useful number.
- 3In our lab, three tests reached 100% statement and branch coverage, yet a real boundary bug at exactly ₹500 survived.
- 4Test coverage is a different metric: requirements (or test conditions) covered by tests, divided by the total.
- 5Use low coverage to find untested code; never use high coverage as proof of quality.
Common mistakes to avoid
- Treating 100% coverage as "fully tested".
- Chasing a coverage target with tests that run code but assert nothing.
- Reporting only line coverage, which hides untaken else-branches.
- Confusing code coverage (from a tool) with test coverage (from requirements).
- Ignoring short-circuit conditions like a || b, where b may never be evaluated.
