Test coverage

On this page 7

Test coverage is a measure of how much of a program a test suite actually exercises when it runs, usually expressed as the percentage of lines, statements, branches, functions or requirements reached by at least one test. In programming and testing, it answers a narrow but useful question: which parts of the code were never executed by any test?

Coverage is a diagnostic signal, not a quality score. A line that ran during a test was not necessarily checked by an assertion, so high coverage shows that code was reached, while only good assertions show that its behavior was verified. Teams use coverage to find untested code, guide review and stop regressions in test effort, rather than to prove correctness.

How test coverage is measured

Coverage tools work by instrumenting the program, either by inserting counters into source code or bytecode before execution or by using runtime hooks that record which instructions run. After the test suite finishes, the tool compares the set of executed locations with the set of all executable locations and produces a report, typically in HTML, XML or the LCOV text format.

Instrumentation has a cost. Collecting coverage commonly makes test runs noticeably slower, often in the range of tens of percent and sometimes more for tight loops, and it can change timing enough to hide or expose race conditions. For that reason many teams collect coverage in a dedicated CI job rather than in every local run.

The table below lists common ways tools collect coverage data and the trade-offs of each approach.

TechniqueHow it worksTypical trade-off
Source instrumentationCounters inserted into source before compilingPrecise mapping, needs a rebuild
Bytecode instrumentationCounters added to compiled bytecode at load timeNo source changes, JVM or .NET specific
Runtime tracingInterpreter hooks record executed linesEasy to enable, slower for hot code
Compiler-based coverageCompiler emits profiling and coverage dataLow overhead, needs special build flags
Coverage collection techniques

Types of coverage criteria

Coverage is not a single number, because each coverage criterion defines a different unit of counting. Weaker criteria are cheap to satisfy and easy to report; stronger criteria demand more tests but reveal untested decision logic that line counts hide.

The following table compares the main structural criteria, from weakest to strongest.

CriterionWhat is countedWhat it can miss
Function coverageFunctions called at least onceAlmost all internal logic
Line or statement coverageExecutable lines or statements runThe untaken side of a condition
Branch or decision coverageEach true and false outcome of a decisionIndividual conditions inside compound tests
Condition coverageEach boolean sub-condition true and falseSome decision outcomes
MC/DCEach condition shown to affect the outcome independentlyData-dependent and timing defects
Path coverageEvery distinct path through a functionRarely achievable with loops
Structural coverage criteria compared

Branch coverage is the most widely used step beyond lines. A statement such as if (a && b) can reach full line coverage with one test, while branch coverage needs a case where the decision is false. MC/DC, modified condition/decision coverage, is required for the most critical airborne software under the DO-178C standard, and it needs roughly one test more than the number of conditions per decision.

Beyond code structure, teams also track requirements coverage, which maps tests to specified features, and mutation score, which measures how many deliberately injected faults the tests detect. Mutation testing is the closest common metric to assertion strength, but it is far more expensive to run.

How coverage relates to unit, integration and end-to-end tests

Each test level contributes coverage differently, so merged coverage reports give a more honest picture than a figure from one level alone. A unit test produces focused coverage of a single module, while an end-to-end test touches large amounts of code with little verification of each part.

Test levelCoverage per testPrecision of verification
Unit testSmall, targetedHigh, one behavior per test
Integration testMedium, across module boundariesModerate, checks interactions
End-to-end testLarge, many layers at onceLow per line, checks user flows
Coverage contribution by test level

A heavily used mock can inflate unit coverage of the caller while leaving the real dependency unexecuted. In TDD, coverage tends to be high as a side effect, because no production code is written without a failing test first; the metric then confirms discipline rather than driving it.

Setting and using coverage targets

There is no universal correct percentage. Many teams settle on 70 to 90 percent line or branch coverage for application code, accept less for glue code and user interface layers, and demand more for core libraries, parsers and financial or safety logic. The right level depends on risk, change frequency and cost of failure.

The most effective policy is often a coverage ratchet or diff coverage rule: new and changed lines must be covered at a set level, and the overall figure may not drop. This focuses effort where defects are introduced instead of forcing tests for stable legacy code.

SituationRecommended approachReason
New service or libraryBranch coverage target around 80 percentCheap to reach while code is fresh
Large legacy codebaseDiff coverage on changed lines onlyAvoids low-value tests on stable code
Safety-critical moduleMC/DC plus requirements tracingRequired by certification standards
Prototype or spikeNo formal targetCode is expected to be discarded
Coverage policy by situation

Coverage reports are most valuable during code review. An uncovered error handler or an untested else branch in a pull request is a concrete prompt for a missing test, which is a better use of the data than a single dashboard number.

Limitations and common mistakes

The central limitation is that execution is not verification. A test that calls a function and asserts nothing yields full coverage of that function and catches almost no defects. When coverage becomes a hard target, teams tend to produce such assertion-free tests, an example of a metric losing value once it is gamed.

  • Missing code cannot be uncovered: a forgotten validation check has no lines to report.
  • 100 percent coverage does not mean all input combinations, boundary values or concurrency orders were tested.
  • Excluded files such as generated code or configuration can quietly raise the reported figure.
  • Flaky or skipped tests can make coverage fluctuate between runs without any code change.
  • Averages hide hotspots: a project at 85 percent may have a critical module at 30 percent.

Treat a coverage drop as a question for review, not an automatic failure, and pair coverage with mutation testing or careful review of assertions when correctness really matters.

In simple terms

Test coverage tells you how much of a program your tests actually touched. Imagine a building inspector who walks through a house with a checklist. Coverage reports which rooms the inspector entered. It does not say whether the inspector looked carefully at the wiring in each room.

So a high number is good news about where tests went, but not proof that the software works. Its real value is pointing at the rooms nobody entered at all, so the team knows where to look next.

A practical example

A team maintaining a payment service runs its test suite in CI with branch coverage enabled. The overall figure sits at 82 percent, which looks healthy. A developer opens a pull request that adds currency rounding, and the diff coverage check reports that only 60 percent of the new branches ran, below the team rule of 80 percent.

The report highlights an untested branch that handles currencies with three decimal places. The developer writes two unit tests for that case, and one of them fails because the rounding used two decimals. The defect is fixed before release, and the pull request merges with full branch coverage of the changed lines. The number mattered less than the specific uncovered branch it exposed.