Line, function, and branch coverage: what the numbers actually mean
Your CI shows 82% coverage. Is that good? Depends entirely on which metric you're measuring — and most teams are measuring the wrong one.
Most teams report a single coverage number — "we have 82% coverage" — without specifying which metric they mean. That number can hide critical gaps or mislead you into thinking code is well-tested when it isn't. Understanding the difference between line, function, and branch coverage is not a theoretical concern: it directly affects whether your test suite catches the bugs that matter.
Line coverage
Line coverage (sometimes called statement coverage) measures the percentage of executable lines that were run at least once during your test suite. A line is "covered" if any test caused the program counter to pass through it. It is the easiest metric to understand and the most commonly reported.
In an lcov.info file, line coverage is recorded via DA records:DA:<line_number>,<hit_count>. The totals are LF (lines found) and LH (lines hit). Line coverage = LH / LF × 100.
What it catches: Dead code (lines with hit count 0 have never been executed), completely untested functions, and code that only runs on the happy path.
What it misses: Conditional logic. If you haveif (user.isAdmin) { doA(); } else { doB(); } and your tests only ever pass admin users, line coverage will show 100% for that block — both doA() anddoB() are lines, and they're both reachable via different code paths. But if no test ever exercises the non-admin path, the else branch is untested.
Function coverage
Function coverage measures the percentage of functions (or methods) that were called at least once. In LCOV: FN records list function definitions, FNDA records list hit counts, and FNF/FNH are the totals.
What it catches: Entirely untested functions — code that was written but never exercised. This is the most dramatic gap indicator. A file with 100% line coverage and 60% function coverage has 40% of its functions that were never entered during testing.
What it misses: Everything inside a function that was called. Function coverage is a coarse metric — a function is either "hit" or not. If a function has ten conditional branches and your test only exercises two of them, function coverage still shows 100% for that function.
Function coverage is most useful for identifying dead code. If a function has 0 hits across your entire test suite, either it is dead code (consider deleting it) or it is only reachable via a runtime path your tests don't cover (consider adding a test).
Branch coverage
Branch coverage is the most powerful and least commonly met metric. It measures whether each possible outcome of every conditional expression was taken. For an if/else, branch coverage requires both the true and false paths to be exercised. For a switch with five cases, all five branches must be hit.
In LCOV: BRDA records store branch data — BRDA:<line>,<block>,<branch>,<count>. A count of - means the branch was never taken. Totals are BRF(branches found) and BRH (branches hit).
What it catches: Untested error paths, missed edge cases in validation logic, ignored else branches, and code that only ever runs in the "everything works" scenario. The bugs that reach production almost always live in untested branches.
Why it's hard to hit: Branch coverage grows exponentially with conditional complexity. A function with 5 independent if-statements has 32 possible execution paths. Testing all of them requires exponentially more test cases than line coverage requires. In practice, 70% branch coverage is considered excellent; 80%+ is exceptional.
| Metric | Measures | Misses | Target |
|---|---|---|---|
| Line | Code executed | Conditional paths | ≥ 80% |
| Function | Functions called | Internal branches | ≥ 80% |
| Branch | All conditional paths | Complex path combos | ≥ 70% |
The 80% rule — and why it can mislead
"80% coverage" became an industry standard partly because it sounds rigorous and partly because it is achievable without extreme effort. But 80% line coverage and 80% branch coverage are very different standards. Teams that report "80% coverage" and mean line coverage may have 50% branch coverage — meaning half of their conditional logic has never been tested.
A more meaningful threshold is: 80% line, 80% function, 70% branch. Requiring all three prevents the common pattern of padding line coverage with easy tests while leaving error handling and edge cases untouched.
The most important coverage is not the aggregate number — it is the coverage of high-risk files. Authentication logic, payment processing, data validation, and security-critical code should have close to 100% branch coverage regardless of the overall project average.
How to read these metrics in an LCOV report
When you open an lcov.info file in quickhelp.dev's LCOV Coverage Viewer, each file row shows all three metrics side by side. Sort by branch coverage ascending to find the files with the most untested conditional paths. Files at 100% line coverage but low branch coverage are the most dangerous: they look well-tested but have significant logical gaps.
The viewer colour-codes thresholds: green for ≥ 80%, amber for ≥ 60%, red for below 60%. A file that is red on branch coverage but green on line coverage is telling you exactly where to write your next tests.