Coverage is the share of code that ran at least once while the tests executed. The tool wraps the source in counters and records where control went. That yields several numbers worth telling apart. Line coverage - how many lines executed. Function coverage - how many functions were called. Branch coverage - how many branches of a condition were actually taken: both the if and the else, both sides of a ternary, both outcomes of the short-circuit in a || b. These numbers gather themselves, which is exactly why it is so tempting to read them as a measure of test quality. That is the first trap.
The naive line of thought looks reasonable: the number grows as you add tests, so a higher number means a better suite. Hence the goal of '100 percent' and a CI gate that rejects a pull request below the threshold. One indicator, a clear bar, a sense of movement - convenient for the manager. The trouble is that the metric does not measure what it seems to.
Coverage records execution, not verification. Take a checkout service with an applyDiscount function that computes a discount from the cart total. A test that merely calls applyDiscount(order) and stops there will hand full coverage to those lines and that function - control did pass through them. But no expect compared the result against an expectation. The line ran; nobody looked at what it returned. Coverage is green, and the contract is protected by nothing.
This is why a bare line percentage misleads more than branch coverage. Suppose applyDiscount returns early for an empty cart and then branches on the total threshold. A single test for an ordinary order will run almost every line and show high line coverage - yet never touch the empty-cart branch or the threshold boundary. Branch coverage sees this: the untaken branches are named outright. As a quick signal, branches beat lines, because bugs live precisely in the branches nobody took.
Thresholds are set in coverageThreshold and work as a floor, not a goal. A global minimum keeps the suite from sliding downward, while for critical areas the bar is raised deliberately: the domain, where money and rules live, deserves stricter numbers than the glue around it. coverageProvider: 'v8' takes the data straight from the engine, without instrumenting through Babel. The key word is floor: a threshold keeps things from getting worse, but on its own it does not make the tests any stronger.
The strength of the checks is measured not by coverage but by mutation testing. The tool takes the source and introduces small breakages - mutants: it changes > to >=, removes a line, flips a boolean constant. After each edit it reruns the suite. If the tests stay green on the broken code, the mutant survived - which means the checks are too weak to notice that breakage. The classic case: replacing > with >= at the discount threshold boundary should make the suite fail. If it does not, the boundary contract is unprotected, no matter how much coverage there is.
All of this has a price. Mutation testing reruns the suite dozens of times and is therefore slow - it is kept for the domain and run overnight or on the release branch, not on every commit. A mandatory threshold set too high also does harm: it nudges people to pad the number with tests that carry no assert, just so the gate lets them through. That is exactly what a real failure looks like: a team nails 100 percent lines, adds calls without checks, the report gleams - and in production an order at exactly the threshold amount gets the wrong discount, because nobody checked the >= boundary. Coverage showed that the code was executed. That it was verified it never showed at all.
coverageProvider: 'v8',
coverageReporters: ['text', 'html', 'lcov'],
coverageThreshold: {
global: {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
'./src/domain/': {
branches: 90,
lines: 95,
},
}