Skip to content

What Coverage Actually Measures

The Part overview planted one sentence to hold for everything that follows: a metric is a proxy for confidence, never confidence itself. Coverage is the first — and by far the most trusted — of those proxies. Almost every team that measures test quality at all starts here, because the tooling is free, the number is a single percentage, and CI can gate on it without a human reading anything.

So before we can judge coverage, we have to know exactly what it is measuring. Not the folklore (“how well-tested the code is”) but the mechanical truth. Because the gap between what people think coverage means and what it actually records is where almost all false confidence in this Part is born.

Here is the definition, with no softening:

Code coverage is the fraction of your code that was executed while your test suite ran.

That is the whole thing. If your codebase has 1,000 executable lines and, across every test you ran, 850 distinct lines were executed at least once, your line coverage is 850 / 1000 = 85%. Coverage does not know what a test is; it does not read your assertions; it does not know whether the output was right. It knows one thing: this line ran, that line did not.

Read the verb carefully. Coverage measures execution, not verification. “This code was covered” means “this code ran during a test.” It does not mean “this code was tested” in the sense you actually care about — that its behaviour was checked and found correct. Holding those two apart is the entire point of this page.

the word "covered" quietly slides between two meanings
────────────────────────────────────────────────────
what coverage records: "this line executed while a test ran"
what you hear: "this line's behaviour was checked"
└── coverage never measures this ──┘

How it is measured — instrumentation, step by step

Section titled “How it is measured — instrumentation, step by step”

Coverage feels magical until you see the mechanism, and the mechanism is mundane. A coverage tool does three concrete things.

Before your tests run, the tool rewrites your program — or its compiled form — to insert counters. Conceptually, it drops a “hit” recorder next to each measurable unit (each line, or each branch). Imagine this function:

def classify(n):
if n < 0:
return "negative"
return "non-negative"

The instrumented version behaves as if every measurable point announces itself when reached:

def classify(n):
HIT(line 1) # entered the function
if n < 0:
HIT(line 2) # the "n < 0 is true" branch
return "negative"
HIT(line 3) # the "fell through" branch
return "non-negative"

You never write these HIT calls and you never see them — the tool injects them into a compiled or bytecode form and strips them afterward. Some tools instrument source, some instrument bytecode (Python’s coverage.py), some read compiler/hardware counters (Go’s -cover, Rust’s -C instrument-coverage, gcc’s gcov). The idea is identical: a counter per measurable unit.

Now your test suite runs — completely normally. Each time execution reaches an instrumented point, its counter increments. When the suite finishes, the tool has a raw table:

line hits
───── ────
1 12 ← called by 12 tests
2 0 ← NEVER reached: no test passed a negative n
3 12

Line 2 has zero hits: no test in the whole suite ever called classify with a negative number. That zero is coverage’s real product — a precise, mechanical inventory of what your tests never touched.

Real suites run in many processes — unit tests here, an integration run there, maybe several shards in parallel CI. Each produces its own hit table. The tool merges them: a line counts as covered if it was hit in any run. The final report is the union.

run A hits: {1, 3} (unit tests)
run B hits: {1, 2, 3} (integration tests, one negative case)
──────────────────────
merged: {1, 2, 3} → 3 of 3 lines covered → 100%

That merge is why you can trust one aggregate percentage across a whole test pipeline — and also why a single carefully-added test can lift a project’s number without improving a single existing test.

The central limit: executed is not checked

Section titled “The central limit: executed is not checked”

Everything above tells you how coverage is collected. Here is the single most important thing it can never tell you, and it deserves its own line:

Coverage proves code was executed. It says nothing about whether that code’s behaviour was asserted or correct.

A counter increments when a line runs. It has no idea whether a test then checked the result. This is the same wall we hit earlier in the book as the oracle problem: running the code and knowing the answer was right are two different acts, and coverage only witnesses the first.

A worked example: 100% coverage, zero assertions

Section titled “A worked example: 100% coverage, zero assertions”

Take our classify function and write a “test” that a coverage tool will happily score as fully covering it:

def test_classify_covers_everything():
classify(-1) # executes the n < 0 branch
classify(5) # executes the fall-through branch
# ...and that's it. No assert. Nothing is checked.

Run coverage on this and you get a triumphant 100% — both branches ran. Now break the function:

def classify(n):
if n < 0:
return "non-negative" # BUG: labels reversed
return "negative" # BUG: labels reversed

Re-run the “test.” It still passes. It still reports 100% coverage. The function is now completely wrong and the suite is blind to it, because the test never made a single assert — it executed the lines and threw the results away. Coverage measured execution perfectly and told you nothing about correctness, because there was no correctness check to measure.

assertion-free test real test
─────────────────── ─────────
classify(-1) assert classify(-1) == "negative"
classify(5) assert classify(5) == "non-negative"
│ │
100% coverage 100% coverage
catches reversed-labels bug? NO catches reversed-labels bug? YES
└──────── same coverage number, opposite value ────────┘

The coverage number is identical in both columns. The difference — the entire difference between a worthless test and a good one — is invisible to coverage. This is not a corner case or a tooling bug. It is the definitional ceiling of what coverage can see. The same trap sits inside real suites: the kvlite server tests in rust/kvlite/tests/server.rs only test the SET/GET handlers because each round_trip is followed by an assert on the reply — strip those asserts and the handlers would still be executed, still score full coverage, and check nothing. It is exactly why the next major page introduces a metric (mutation testing) built to catch the assertion-free test that coverage waves through.

So what is coverage good for, if it can be gamed to 100% by a test that checks nothing? This is the thesis the rest of the Part builds on, so plant it now:

Coverage is a guide, not a goal. Its job is to point you at code your tests never touched — not to certify the code they did.

Used as a guide, coverage is genuinely valuable and cheap. A line with zero hits is a hard, trustworthy fact: no test ran it, so it is definitionally untested, and coverage found that for free. That is coverage’s superpower — it reliably flags absence. Read this way, the number is a starting question, not a verdict: “the refund handler shows zero hits — why does no test go there?”

Used as a goal — “hit 90% or the build fails” — coverage inverts. People write assertion-free tests to touch lines, delete hard-to-cover error handlers, and mark risky code as excluded. The number climbs while confidence falls. That failure mode is universal enough that the Part closes on it: Goodhart’s law. Between here and there, coverage types will sharpen what “a unit” even means (lines vs. branches vs. paths), and why 100% is not bug-free will show the bugs coverage cannot see even when it reads 100%.

  • Why does it exist? Because on any system too large to hold in one head, “are the dangerous paths even run by a test?” cannot be answered by reading — you need a mechanical, automatic inventory of what executed. Coverage is that inventory, computed for free from a normal test run.
  • What problem does it solve? It reliably finds untested code — lines with zero hits are, by definition, exercised by no test. It turns “I think we’re covered” into an exact list of what is provably not covered.
  • What are the trade-offs? It measures execution, not verification, so it is trivially gamed: an assertion-free test scores full marks. It buys cheap confidence about absence (what’s untouched) and offers no confidence about quality (whether touched code was checked).
  • When should I avoid it? Never avoid collecting it — it is nearly free. Avoid trusting it as a quality certificate, and avoid making a coverage percentage a hard target, which converts it from a guide into a Goodhart trap.
  • What breaks if I remove it? You lose the cheapest possible signal of untested code. Whole modules can drift with no test touching them and nothing will flag it — the zero-hit lines coverage would have surfaced instead ship silently to production.
  1. State, in one sentence, exactly what a coverage percentage measures — using the word executed.
  2. Walk through the three mechanical steps a coverage tool performs, from before the tests run to the final report.
  3. A test calls a function with two different inputs but contains no assert. What line coverage will it report for that function, and what does that number prove about correctness?
  4. Coverage’s most trustworthy output is arguably a zero, not the headline percentage. Why is a zero-hit line a stronger, more reliable fact than “92% covered”?
  5. Restate the “guide, not goal” thesis, and explain what goes wrong the moment a coverage percentage is turned into a hard CI target.
Show answers
  1. Coverage is the fraction of your code that was executed at least once while your test suite ran — nothing more; it counts lines/branches that ran, not behaviours that were checked.
  2. (1) Instrument: the tool rewrites the code (source, bytecode, or via compiler counters) to insert a hit-counter at each measurable unit. (2) Run and record: the test suite runs normally; each counter increments when its point is reached, producing a raw hits-per-unit table. (3) Merge: hit tables from every process/run are unioned — a unit counts as covered if hit in any run — into one report, whose most useful column is the list of missed units.
  3. It can report up to 100% line coverage for that function (both inputs execute the lines). The number proves the lines ran; it proves nothing about correctness, because with no assertion the test discards the results — the function could be entirely wrong and the test would still pass at 100% coverage.
  4. A zero-hit line is a definitional fact: no test executed it, so it is provably untested, with no ambiguity to interpret. “92% covered” is a blend that hides which lines and — worse — whether the covered lines were actually asserted on; the percentage can be high while the tests check nothing, but a zero cannot lie about absence.
  5. Guide, not goal: coverage’s real job is to point you at code no test touches (a starting question), not to certify the code tests do touch (a verdict). Turned into a hard target (“90% or the build fails”), it triggers Goodhart’s law — people write assertion-free tests to touch lines, delete hard-to-cover branches, or exclude risky code; the number rises to satisfy the gate while the confidence it was meant to represent falls.