Coverage answers exactly one question: which lines of code executed during the test run. This must be understood from the start, because a dangerous misconception has grown around the metric. Coverage shows that a line was executed - and says nothing about whether its correctness was checked. A line can run inside a test with not a single assert on its result and still count as covered.
In Vitest coverage is enabled by a provider. By default it is v8 - it takes data straight from the V8 engine, is fast and requires no code instrumentation. Reporters are chosen for the task: text prints a table to the terminal, html gives a clickable report with uncovered lines highlighted, lcov is a machine format for CI and external services. Usually several are enabled at once, each for its consumer.
Thresholds set a minimum below which the build fails. Their role is precisely a floor, insurance against sliding down, not a goal to reach for. The difference is fundamental: a threshold of 80% does not mean 80 is a good grade; it means you cannot drop below 80 without an explicit decision. As a goal, a coverage percentage is harmful - it pushes you to write tests for lines rather than for behavior.
Far more honest than lines are branches. Line coverage is easy to fool: a single happy-path test runs a function top to bottom and yields a high percentage without ever entering the else, the error branch, the empty-input handling. Branch coverage shows whether all forks were taken, and it is exactly what exposes unchecked branches - the very places where bugs usually hide.
But even a hundred-percent branch coverage does not guarantee the tests catch anything. Mutation testing proves this: the tool introduces a small breakage into the code - changes > to >=, + to -, drops a call - and watches whether at least one test goes red. If after the corruption all tests are green, then the line executes but nobody checks its result, and coverage over it is an empty number.
The boundary example is telling. The condition if (age > 18) and the condition if (age >= 18) give different behavior at exactly one point - when age equals 18. A test that hits only age 25 and age 10 will cover the line and both branches a hundred percent, but will not notice the > to >= swap: the boundary stayed unprotected. The mutant survives - and honestly reports that there is no boundary test.
Hence a sober view of the metric. Coverage is useful as an indicator of the untraveled: red lines in the html report show directly where a test has not yet looked, and that is valuable. But a high percentage is not proof of quality; it is only an upper bound on what the tests could in principle have checked. A suite's real strength is measured by whether it catches introduced breakages, not by how many lines lit up green.
The typical failure is chasing the number: adding tests without asserts just to color the lines, and getting a high percentage with a powerless suite. The second failure is looking only at lines, ignoring branches: the report shows 90%, while half the conditions never ran both ways. Keep thresholds a floor, read branches, and verify the checks' quality with a mutation run, not with a percentage.
// vitest.config.ts -> test:
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: { // a floor, not a goal
lines: 80,
branches: 80, // branches are more honest than lines
functions: 80,
},
}// Both tests give 100% of lines and branches...
expect(canVote(25)).toBe(true)
expect(canVote(10)).toBe(false)
// ...but do not catch swapping age > 18 for age >= 18: no test at the 18 boundary.
// The mutant survives -> the boundary is unprotected.