Skip to main content
Coverage Is a Map, Not a Safety Net
Back to blog

Coverage Is a Map, Not a Safety Net

0% coverage does not identify dead error handlers. Teams treat coverage as a quality metric: 80% means reasonably tested, 60% means work remains, and 90% means you are doing well. This framing is not wrong, but it misses coverage's most useful signal.

Coverage shows, with high fidelity, which code lines the test suite has run. As a quality measure, you focus on covered lines. As a removal guide, you focus on lines the suite has not run.

Coverage and Dead Code Overlap

Uncovered code is not always dead. Some is live code on paths the test suite exercises poorly: error handlers, fallback conditions, and infrequently taken branches. Those gaps point to testing deficiencies, not dead code.

Still, in a mature codebase, a meaningful share of uncovered code is not a testing gap. The test suite does not call it because nothing calls it. Static analysis supports that conclusion when no production entry point reaches it. Coverage supports it when no test executes it. Together, unreachable by static analysis and never executed by tests, those signals strongly indicate dead code.

YAGNI uses this overlap as a signal layer, not a deterministic verdict. A function marked dead by call graph analysis with 0% coverage across the entire test suite is a higher-confidence finding than one marked dead by call graph analysis with partial coverage. Coverage cross-validates the static analysis.

Read Coverage Data Differently

Normally, you read a coverage report by checking its percentage and seeing what was covered. For removal, reverse that order.

Begin with files and modules at zero coverage. This is not about a zero-coverage line inside a partly covered file, but modules where no line has ever been hit. They offer the clearest removal story. If neither tests touch a module nor any production path reaches it, ask whether an external mechanism not visible to the analyzer runs it.

Then inspect functions with zero coverage in files that have some coverage. This is mixed liveness: some functions in a module are tested and live, while others are not. Such functions are often remnants of old features, replaced implementations left beside their replacements, or speculative utilities that were never used.

Last, inspect branches inside covered functions that are never taken. A function with 100% line coverage but 40% branch coverage may contain dead branches: error paths for conditions that never occur, feature-flag branches for a flag permanently turned off, or code handling an enum value removed from the possible set.

Uncovered Is Not the Same as Unreachable

Coverage can mislead in one key way: a reachable function your test suite does not exercise looks identical in the report to one that is genuinely unreachable. Both appear uncovered.

That is why coverage alone is not a safe removal guide. Deleting everything with zero coverage would also delete live code your tests do not reach. Filter coverage through call graph analysis: only zero-coverage code that is also statically unreachable from any production entry point is a clean removal candidate.

The reverse case matters too: when call graph analysis marks a function dead but coverage shows tests exercising it, investigate. The tests may call a private function directly, bypassing normal call paths, or the call graph analysis may be wrong. Either case needs review before removal.

Coverage Data for Sequencing Work

Coverage also helps sequence removal work. Be more cautious with removal candidates beside modules with very low overall coverage. If 30% of live code in the area is also uncovered by tests, a mistake has a larger blast radius, and test failures will not catch it. Work there should move more slowly and receive more careful human review.

Removal candidates near highly covered modules are safer to automate. Tests passing through adjacent live code can catch a mistake if a supposedly dead function is actually called from live code in a way the analyzer missed.

Use Coverage to Prioritize Test Work

Coverage analysis consistently reveals live code without test protection. That is adjacent to, but distinct from, removal work. The same analysis that finds removal candidates also exposes live paths with no test coverage at all.

This is not dead code, but another kind of engineering debt. Separating it during removal is useful: removal improves the codebase directly, while uncovered live paths become a test-improvement backlog that makes later removals safer.

Read as a map instead of a grade, the coverage report shows what you can clean up now and where better tests are needed before safe cleanup later. That gives you more than a percentage.

What Coverage Cannot Show

Automated test-suite coverage reports miss manual testing, exploratory testing, and developer console usage. Where engineers often run functions manually for debugging or validation, those functions may look completely untested by the automated suite while remaining actively useful.

That limitation affects the signal, not its trustworthiness. Coverage plus static analysis should generate candidates for review, not verdicts for automated deletion. Human review catches gaps caused by manual usage patterns rather than genuine deadness.

The coverage map is authoritative about what the test suite reached, but not about what a person or system outside that suite reached. Keeping both facts in view makes coverage useful without making it dangerous.

Try YAGNI on your codebase. Free plan, no credit card required.

Get Early Access

More from the blog

View all articles