NodeJSCode Coverage

Code Coverage

Code coverage measures how much of your code the test suite actually executes — which lines, branches, and functions ran during the tests. It's a useful diagnostic for finding untested code, and a common CI gate. But it's also one of the most misunderstood metrics: high coverage does not mean good tests, and chasing 100% can produce worthless tests that execute code without verifying anything. This page covers the coverage types, the tooling, and — most importantly — how to use coverage well.

The four coverage metrics

Metric

Measures

Line

Which lines of code were executed

Branch

Which branches of conditionals (if/else, ?:, &&) were taken

Function

Which functions were called

Statement

Which statements were executed (finer than line)

Branch coverage is the most meaningful — it checks both sides of every decision
Line coverage can be misleadingly high: a single-line `if (x) doA(); else doB();` shows as "covered" if only one branch ran. **Branch coverage** is stricter — it requires both the `if` and the `else` paths to execute, catching the untested error path or edge case. When you set a coverage threshold, branch coverage is the most valuable number to gate on because it reflects whether you've tested the *decisions* your code makes, not just that the happy path runs.
Generating coverage

Bash
# Jest — built in:
jest --coverage

# node:test — built in:
node --test --experimental-test-coverage

# Mocha — via c8 (or nyc):
npm install --save-dev c8
c8 mocha
----------------|---------|----------|---------|---------|-------------------
File            | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files       |   87.5  |   76.92  |   90.0  |   87.5  |
 pricing.js     |   100   |   100    |   100   |   100   |
 userService.js |   80.0  |   66.67  |   85.7  |   80.0  | 42-48, 71
----------------|---------|----------|---------|---------|-------------------
`c8` uses V8's native coverage — fast and accurate for any runner
`c8` leverages Node's built-in V8 coverage instrumentation rather than transpiling your code, making it fast and accurate. It works with any test runner (wrap the command: `c8 mocha`, `c8 node --test`). The older `nyc` (Istanbul) does source instrumentation and is still widely used. Jest and `node:test` have coverage built in. The "Uncovered Line #s" column is the most actionable output — it points you directly at code no test touches.
Setting a coverage threshold in CI

jest.config.js

JS
export default {
  collectCoverage: true,
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    // Stricter for critical modules:
    './src/auth/': { branches: 95, functions: 95, lines: 95 },
  },
  // Don't count files that don't need testing:
  coveragePathIgnorePatterns: ['/node_modules/', '/migrations/', '/config/'],
}
Gate on a realistic threshold and ratchet it up — don't demand 100% from day one
A coverage threshold in CI prevents new untested code from sneaking in — if a PR drops coverage below the bar, the build fails. Set it at a level your suite already meets (e.g. 80%) and **ratchet it up** over time rather than mandating 100% immediately. You can set higher thresholds for critical modules (auth, payments, billing) where untested branches are most dangerous, and exclude code that genuinely doesn't need unit tests (generated files, migrations, config).
The 100% coverage trap

JS
// This test gives 100% line coverage of add()...
test('add runs', () => {
  add(2, 3)            // ❌ no assertion! Coverage is 100%, but it tests NOTHING
})

// ...but this is the test that actually matters:
test('add returns the sum', () => {
  assert.equal(add(2, 3), 5)   // ✅ verifies behaviour, not just execution
})
Coverage measures execution, NOT correctness — 100% coverage can still test nothing
The fundamental limitation: coverage tells you a line *ran*, not that you *verified its output*. A test that calls a function and asserts nothing achieves full coverage while testing nothing. Worse, chasing a 100% target incentivises exactly these worthless tests — and tests written to hit a number rather than verify behaviour create maintenance burden without safety. Use coverage to find *gaps* (code no test touches, likely a missing test), not as a quality score. A suite with 70% coverage and meaningful assertions beats one with 100% coverage and none.
Using coverage well
  • Find gaps, don't chase a number — use the uncovered-lines report to spot missing tests.

  • Prioritise branch coverage — it reveals untested decision paths.

  • Higher bars for critical code — auth, payments, data integrity.

  • Exclude code that doesn't need tests — generated files, config, migrations.

  • Every covered line should have a meaningful assertion behind it.

  • Coverage is a floor, not a ceiling — 80% with good tests beats 100% with empty ones.

Next
Write the test before the code: [Test-Driven Development](/nodejs/tdd).