All topics
Testingintermediate

Code Coverage and Its Limitations

What code coverage metrics actually measure (lines/branches executed by tests), and why high coverage numbers don't guarantee genuinely good tests.

Coverage measures which lines and branches the tests executed. It does not measure whether they asserted anything.

Code coverage is like tracking what percentage of a building's rooms someone physically walked through during a fire inspection — 100% room coverage tells you every room was visited at least once, but says absolutely nothing about whether the inspector actually checked each room's smoke detectors properly or just walked in, glanced around without looking at anything specific, and walked back out.

Key Concepts

1
    statements   which statements ran
    branches     which sides of each if / ternary ran
    functions    which functions were called
    lines        which lines ran
2
A test with no assertions gives full coverage.
    it('works', () => {
      calculateTotal(order);      // no expect at all
    });
3
Every line runs. Coverage says 100%. The function could return anything.
4
And covered code can still be wrong.
    function divide(a, b) { return a / b; }
5
    it('divides', () => expect(divide(10, 2)).toBe(5));
6
One line, fully covered. divide(1, 0) returns Infinity and nothing tests it.
divide(1, 0)Infinity
7
So coverage tells you where tests are absent, not where they are good. That asymmetry is the whole point:
8
    low coverage    definitely untested code -- actionable
    high coverage   the tests ran the code. Nothing more.
9
Branch coverage is the more useful number.
    if (user && user.isActive && user.hasSubscription)
10
Line coverage reaches 100% with one test. Branch coverage shows the combinations that were never tried, which is where the bugs are.
11
What a target does to a team. A mandated 80% produces tests written to reach 80% — calling functions without asserting, testing getters, covering error paths with a try and no check.
try
12
The metric is met and the suite is worse than none, because it gives false confidence.
13
How to use it properly.
    look at the report, not the number
    find uncovered branches in important code
    exclude generated files and configuration
    use it as a prompt: "is this untested on purpose?"
14
And the cases where low coverage is correct. Configuration, generated code, thin wrappers over a library, and anything where the test would be a restatement of the implementation.
15
The question that matters more. If I break this deliberately, does a test fail? That is what mutation testing automates — and it measures what coverage is usually assumed to measure.
16
What the interviewer is probing.1. "Is 100% coverage a good target?" Probing: whether you can push back. Stalls: "Yes." Moves up: no — it measures lines executed, not assertions made; a test with no expect still covers the line.
17
2. "What does branch coverage add over line coverage?" Probing: the distinction. Stalls: "Nothing." Moves up: it checks both sides of each condition, so an untested else is visible where line coverage may look complete.
18
3. "How can coverage mislead?" Probing: the gap. Stalls: "It cannot." Moves up: it shows what ran, not what was verified, and says nothing about the inputs you never tried.
19
4. "What is coverage genuinely useful for?" Probing: the right use. Stalls: "A target." Moves up: finding code no test touches at all, which is a far better signal than the percentage.