You can have 100% coverage and zero useful tests. Here is the proof.
export function divide(a: number, b: number): number {
return a / b;
}
test("divide works", () => {
divide(10, 2);
});
Every line of divide is executed. The coverage tool reports 100%. The test asserts nothing. A regression that made divide return "banana" would pass.
Now extend this pattern across 10,000 functions in a real codebase and you have a 100% number that tells you the tests run without crashing. That is not what coverage is supposed to measure, but it is what a naive coverage percentage actually captures.
What coverage does measure
Line coverage records which lines were executed by the test suite. High line coverage means your tests reach most of your code. It does not tell you whether they check the behaviour of that code.
Branch coverage records which conditional branches were taken. Better than line coverage, but still only about execution. Two branches can both be covered by tests that assert nothing.
Statement coverage, function coverage: variants of the above. Same limitation. They measure what the test touched, not what the test verified.
What coverage does NOT measure
Assertion density. A test with five expect calls is doing more than a test with zero. Coverage does not distinguish them.
Meaningful assertions. expect(x).toBeDefined() passes on anything that is not undefined. A tautological assertion is worse than no assertion because it hides the absence of a real check.
Edge cases. A test that exercises the happy path and nothing else reports 100% coverage on that function. It catches nothing that happens outside the happy path.
Input validity. Tests that use only well-formed inputs never exercise error handling, even if the error handling is present and covered.
Behaviour preservation. The question "does this function do what it is supposed to do?" is not a coverage question. It is a test quality question.
Two codebases at 80%
Codebase A: tests use property-based inputs, assert on return values and side effects, include edge cases, fail meaningfully when behaviour changes.
Codebase B: tests import the code, call each function once with typical inputs, assert nothing beyond "did not throw".
Both are at 80%. Coverage tools rate them identically. They are not remotely similar. Codebase A catches regressions. Codebase B tells you the code compiles.
Better signals to pair with coverage
Not "instead of". Coverage is still useful as a floor. It needs company.
Mutation testing. Tools like Stryker, mutmut and PIT deliberately change your code in small ways (flip operators, change constants, remove lines) and see whether any test fails. Coverage says "a test ran this line". Mutation testing says "a test would fail if this line broke". That is a fundamentally different and more useful claim. The downside is runtime: mutation testing is slow.
Assertion counts. Simple heuristic: a test file with more assertions per function is more likely to catch regressions. Not perfect, but tracking this over time reveals when test quality is regressing.
Branch coverage over line coverage. More expensive to achieve, closer to meaningful.
Reviewing tests in PRs. Humans can tell when a test is shallow. Most teams skim tests during review. The teams where coverage is genuinely high quality are the ones where reviewers treat tests as code worth reading carefully.
When the 100% goal is actively harmful
On some projects, yes, aim for very high coverage. Safety-critical code. Financial infrastructure. Auth flows. Payment processing. Anything where a bug has material consequences.
On most projects, chasing 100% is a bad trade. Tests for trivial getters and setters. Tests for glue code that calls two other well-tested functions. Tests for UI components whose behaviour is better verified by end-to-end tests elsewhere. The cost of maintaining these tests is high and the regression protection is low.
A team that aims for 80% meaningful coverage is usually ahead of a team that aims for 100% at any cost.
The right question
"How much of my code is covered by tests?" is the wrong question. The right question is "how confident am I that my test suite will catch the next bug before users do?"
Coverage helps answer the first question. It barely touches the second.
Treat coverage as a floor. Below some percentage, the suite is too thin to take seriously. Above that floor, move to signals that actually correlate with regression detection: mutation scores, meaningful assertions, reviewed tests.
The number in your CI dashboard is a starting point, not a grade.