Blog · Learn · 2026-08-17

What Is Test Coverage? Types, Metrics, and Its Limits

Klavity
TL;DRTest coverage is the percentage of your code that runs while your tests run — measured as line, branch, function, or statement coverage. It tells you what code was executed, not whether a test would actually catch a bug there, so a high number can still hide untested logic and missing assertions.

Test coverage is the percentage of your code that is executed while your test suite runs. It's reported as line, branch, function, or statement coverage, and a coverage tool measures it by instrumenting your code and recording which parts run during tests. Crucially, coverage tells you what code was executed — not whether a test would actually fail if that code broke — so it's a useful gauge of gaps but a poor proxy for quality.

How is test coverage calculated?

A coverage tool instruments your code before the tests run, tracks which lines, branches, or functions execute, and divides what ran by the total available. The basic formula is:

  • Coverage % = (units executed by tests ÷ total units) × 100

The “unit” changes depending on which metric you ask for. If 180 of a file's 240 executable lines run during your tests, line coverage is 75%. The number is generated automatically — in JavaScript with tools like Istanbul/nyc or Vitest's built-in coverage, in Python with coverage.py, in Java with JaCoCo — and is usually enforced in CI with a minimum threshold that fails the build if coverage drops below it.

Types of test coverage: line, branch, function, and statement

“Coverage” is really four different measurements, and they are not interchangeable. Ordered from weakest to strictest:

  1. Function coverage — did each function or method get called at least once? The coarsest signal; a function can be called and still have most of its logic untouched.
  2. Statement / line coverage — did each statement or line execute? The most commonly reported number, and the easiest to inflate.
  3. Branch coverage — did every outcome of every decision run: both sides of each if, each arm of a switch, each side of a ternary? This is the metric worth watching.
  4. Condition coverage — within a compound condition like a && b, did each sub-condition take both true and false values? The strictest and least often measured.

Why the distinction matters: consider if (user && user.isAdmin) grantAccess(). A single test that passes an admin user executes that line, so line coverage reports 100% for it. But the false branch — a non-admin, or a missing user — never ran. Branch coverage would report 50% and point straight at the untested path where real bugs hide.

Why 100% test coverage doesn't mean bug-free

Coverage measures execution, not verification, and that gap is where teams get a false sense of safety. Three concrete reasons a green coverage report can still ship bugs:

  1. Covered is not asserted. A test can call a function, run every line, and assert nothing about the output. The lines count as covered while their behavior is completely unchecked. Coverage can't tell the difference between a real assertion and a bare function call.
  2. You can't cover code you didn't write. Missing error handling, an unhandled null, a forgotten edge case — there's no line to instrument, so coverage stays silent. Coverage only measures the code that exists, never the code that should.
  3. The bug is in the requirement, not the code. If the spec is wrong, a fully covered feature faithfully implements the wrong behavior. No coverage metric catches a correct implementation of a bad idea.

A useful sanity check is mutation testing: it deliberately introduces small bugs (flip a > to >=, delete a line) and checks whether any test fails. If your tests still pass after the code is broken, that “covered” code was never really tested — no matter what the percentage said.

What test coverage percentage should you aim for?

There is no universally correct number, and chasing one is how coverage metrics get gamed. What matters far more than the headline figure is where the coverage sits. Practical guidance:

  • Cover critical paths first. Authentication, payments, data writes, and error handling deserve near-complete branch coverage. A checkout flow at 100% and a settings page at 40% is a healthier suite than a flat 80% everywhere.
  • Treat a hard threshold as a floor, not a goal. Setting a CI minimum (say, “don't let coverage fall below its current level”) stops silent regressions. Setting a mandate of 100% pushes engineers to write assertion-free tests just to hit the number.
  • Watch the trend, not the absolute. Coverage dropping five points in a PR is a signal worth a comment; a stable 72% is usually fine. The direction tells you more than the value.

Many teams land around 70–80% coverage of meaningful code and call that healthy — but read that as a rough resting point, not a target to defend.

How to use test coverage well: 4 rules

  1. Report branch coverage, not just line coverage. Configure your tool to surface branches so partially-tested conditionals stop hiding behind a green line number.
  2. Set a floor in CI, review the diff. Fail the build if coverage regresses, and look at which lines a PR left uncovered rather than only the aggregate percentage.
  3. Spend coverage where failure is expensive. Prioritize money, data, auth, and end-to-end user journeys. For those full flows, self-healing end-to-end tests keep coverage of real paths intact even when the UI changes and selectors would otherwise break.
  4. Close the gap the metric can't see. Coverage misses the code you didn't think to write. When a real user hits an uncovered path in production, capture it as a reproducible report — steps, console, and network in one shot with Snap — and turn that bug into a regression test so the gap closes for good.

Used this way, coverage becomes a map of what your tests touch rather than a scoreboard to win. For more on how the different test types fit together, see our guide to the testing pyramid.

Key takeaways

  • Read coverage as 'what ran,' not 'what's verified' — pair it with real assertions.
  • Prefer branch coverage over line coverage; it exposes untested true/false paths.
  • Aim coverage at critical paths and error handling, not a blanket percentage.
  • Use a coverage floor in CI to prevent regressions, not a number to game.

FAQ

What is a good test coverage percentage?

There's no universal number. Many teams treat 70–80% coverage of meaningful code as healthy, but the figure matters less than what it covers: 60% that exercises your critical paths and error handling is worth more than 95% dominated by trivial getters. Set a floor to stop coverage from sliding, not a ceiling to chase.

What's the difference between line and branch coverage?

Line coverage counts whether a line of code ran at all. Branch coverage counts whether every outcome of each decision ran — both the true and false side of every if, each case of a switch. A single test can hit 100% line coverage on a conditional while exercising only one branch, so branch coverage is the stricter, more honest measure.

Does 100% test coverage mean no bugs?

No. Coverage measures execution, not verification. A test can run a line and still assert nothing about the result, so the line counts as covered while its behavior is unchecked. Coverage also can't catch missing features, wrong requirements, or bugs in code paths you never thought to write a test for.

Catch bugs the moment a human sees them

Klavity: right-click bug reports, AI personas that review your product, and self-healing tests.

Get started free