skip to content

Code Coverage Metrics

Line and statement, branch and decision, path, and function coverage: what each one actually counts, and how instrumentation gathers it. Asked so you can explain why 100% line coverage can still miss half the branches.

on this pageshow

questions

3

What is the difference between line coverage and branch coverage in a test run?

level: juniorimportance: must knowfreq 84%

answer

  1. Two metrics, two different denominators
  2. One line can hide a whole decision
  3. Outcomes taken, not lines touched
  4. Fourteen guards, one test, half the outcomes
  5. Decision coverage subsumes statement coverage

basics

~20 s

Line coverage counts source lines executed at least once. Branch coverage counts which outcomes of each decision were taken, the true side and the false side. A line can execute while one of its two outcomes never runs.

solid answer

~40 s

Line coverage asks whether a source line executed at least once; branch or decision coverage asks, for every decision point, whether every outcome of it was taken. Because a whole decision can sit inside a single line, a suite can execute every line while exercising only one side of each condition. The standard demonstration is a scoring function built from fourteen single-line guards, driven by one test that satisfies all fourteen: 100% line coverage, 50% branch coverage. In structured code full decision coverage subsumes statement coverage, so branch is the stricter of the two and the one worth reading first. Method or function coverage is coarser still, recording only that a function was entered. All three are structural: they describe what ran, not what was checked.

code

pseudocode · 10 lines
pseudocode
function completenessScore(form):
    score = 0
    if form.weightKg != null: score = score + 1
    if form.bpSystolic != null: score = score + 1
    if form.adverseEventNote != "": score = score + 1
    return score

test_fullyFilledForm():
    form = visitForm(weightKg = 71.4, bpSystolic = 118, adverseEventNote = "none")
    assert completenessScore(form) == 3

go deeper

for a junior

Be ready to define each metric in one sentence and give the standard example where every line runs but only one side of each condition is taken. Knowing which denominator each percentage uses is most of the answer.

for a middle

Explain how a decision hides inside a single line — single-line guards, short-circuiting operators, conditional expressions, unwritten else arms — and why full decision coverage subsumes statement coverage in structured code while the reverse fails.

for a senior

Expect to read a real report aloud: which lines are only partially covered, where the branch gap concentrates, and whether a loop or exception edge explains it. Say plainly what neither metric can see, such as iteration counts.

for a principal

Own which structural metric a team reads by default and what its denominator does to comparisons. Branch percentages across a straight-line mapping module and a conditional-dense validation module measure different things and should not be ranked side by side.

## What structural coverage measures Structural coverage answers one narrow question: which parts of the production code did this test run execute? Every metric in the family shares that definition and differs only in what it counts as "a part" — a line, a statement, a decision outcome, a function. Understanding the differences is really understanding the denominators. ### Line and statement coverage A statement is one executable instruction; a line is a unit of source text. They are not the same unit: one line can hold several statements, and one statement can span several lines. Statement coverage is executed statements over executable statements. Line coverage is lines containing at least one executed statement over lines containing at least one executable statement. Blank lines, comments and declarations that compile to nothing are excluded from the denominator, which is why the percentage you see is never "percentage of the file". Line coverage is popular because it maps straight onto the editor gutter. It is also the weakest of the useful signals, because a line is a unit of text, not a unit of control flow. ### Branch, or decision, coverage A decision is any point where control can continue in more than one direction: a conditional, the continuation test of a loop, a multi-way selection, an exception edge, a conditional expression, or a short-circuiting boolean operator. Every decision has at least two outcomes. Branch coverage counts outcomes taken over outcomes available, so its denominator tracks roughly twice the number of decisions rather than the number of lines. Two consequences follow. First, a line can be executed while only one of the outcomes on it has been taken; many reports show this as a distinct third state, "partially covered", beside covered and uncovered. Second, in structured code full decision coverage subsumes statement coverage: if every outcome of every decision has been taken, every reachable statement has been reached. The reverse does not hold, and that asymmetry is the point of the question. ### The canonical demonstration A completeness scorer for a clinical-trial visit form has fourteen optional fields, each guarded by a single-line conditional that increments a counter. One test submits a completely filled form. Every line executes, so line coverage reads 100%. Only the true outcome of each guard was taken, so branch coverage reads 14 of 28 outcomes, 50%. The untouched half is exactly the interesting half — what the scorer does when a field is missing. A report showing 91.7% line and 58.2% branch on that module is telling the same story in a less dramatic form, and the branch column is where the untested behaviour hides. ### Where the gap comes from in practice - **Implicit false arms.** A conditional written without an else still has a false outcome in the control-flow graph. - **Short-circuiting operators.** A two-term boolean expression is two decisions inside one line; a suite that never makes the first term false never takes one of them. - **Conditional expressions** that compile to a branch while occupying a single line. - **Default arms** of a multi-way selection that the source never wrote out. - **Exception edges**, where "the guarded region threw" is an outcome nobody exercises. - **Loop bodies executed zero times** — the exit-immediately outcome of the loop's continuation decision. ### What branch coverage still does not distinguish Branch coverage records that a loop's continuation decision went both ways. It does not record how many iterations ran: a loop that iterates fifteen times and one that iterates once are indistinguishable to it. Nor does it record which combination of conditions produced an outcome; that is condition-level and path-level material, and it is why stricter criteria exist above decision coverage. ### Function and method coverage The coarsest member of the family: a function counts as covered if it was entered at least once. It says nothing about what happened inside — a function entered and returned on its first guard is fully covered by this metric and barely covered by the other two. Its value is triage. On a large or inherited module it shows instantly which whole units no test has ever touched, and it is cheap enough to glance at on every build. Some reports publish a file- or type-level equivalent for the same reason. ### Reading a report without being misled Read the fraction, not only the percentage: 47 of 51 lines and 470 of 510 lines are the same percentage and very different amounts of untested code. Compare like with like — a straight-line data-mapping module and a validation module dense with conditionals have structurally different denominators, so their branch percentages are not directly comparable. And keep in mind what the whole family shares: these metrics observe execution. They record what ran. Whether the run checked anything at all is a different question, answered with different instruments.

  • What does method or function coverage add that line coverage does not?
    It is coarser, not finer: a function counts as covered the moment it is entered, however little of its body ran. Its value is triage on a large module — it names the units no test has ever reached, which line coverage buries inside a single averaged percentage. Never present it as a stricter signal than line or branch coverage; a function that returns on its first guard is 100% covered by it.
  • A loop body runs fifteen times in one test and once in another. Does branch coverage differ between the two runs?
    No. The loop contributes one decision with two outcomes — continue into the body, and exit — and both runs take both. Iteration counts are not part of the metric, which is why boundary bugs at the first or last iteration survive a fully green branch report. If iteration behaviour matters, that has to come from deliberate cases at zero, one and many, not from the coverage percentage.
  • Why do some reports mark a line as only partially covered?
    Because more than one branch outcome lives on that line and the run took some of them. A single-line conditional, a conditional expression, or a short-circuiting boolean operator all put two or more outcomes on one line. The line executed, so it cannot be shown as uncovered; not every outcome was taken, so it cannot be shown as covered. The third state exists to make that visible in the gutter.

Line coverage is a register of which rooms in a building someone walked into; branch coverage is a register of which doors were opened in each direction. You can visit every room without ever having tried the back door.

saying these in an interview costs you the question

  • Says line and branch coverage are the same measure renamed
  • Thinks executing a line means every outcome on it was taken
  • Believes 100% statement coverage implies 100% branch coverage
  • Counts loop iterations as separate branch outcomes
  • Treats function coverage as stricter than line coverage
  • Claims an unwritten else arm has no branch outcome to cover

context

open as a page

How does a coverage tool record which lines and branches actually executed?

level: middleimportance: should knowfreq 48%

basics

~20 s

A coverage tool inserts probes at the entry of each basic block, rewriting either the source before compilation or the compiled artefact at build or load time. Each run dumps the probe hits, which a report step maps back to source lines.

open as a page

Why is full path coverage impractical, and what criteria are used instead?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Path coverage requires exercising every route through a function, and routes multiply exponentially with sequential decisions and become unbounded once loops are involved. Suites use weaker criteria instead: decision coverage, or modified condition/decision coverage where each condition must independently flip the outcome.

open as a page