A suite is trustworthy only when each test starts from a known state. Make the fixture a factory — makeStore(rows = seed) copies its seed rows into a fresh Map and returns the store methods over it — and call it in beforeEach. A test needing an empty catalog writes createApp({ store: makeStore([]) }); one needing a broken dependency passes { get: async () => { throw new Error('down'); } } to check for a 500. Coverage then comes from the runner: --experimental-test-coverage prints a per-file table, and --test-coverage-lines, --test-coverage-branches and --test-coverage-functions turn a shortfall into a non-zero exit code.
$ node --test --experimental-test-coverage # 6 tests, all passing ℹ file | line % | branch % | funcs % | uncovered lines ℹ app.js | 95.00 | 100.00 | 75.00 | 17 ℹ books-controller.js | 84.00 | 50.00 | 80.00 | 9-10 20-21 ℹ store.js | 95.00 | 100.00 | 80.00 | 11 ℹ all files | 90.77 | 72.73 | 78.57 | $ node --test --experimental-test-coverage --test-coverage-lines=95 # exit code 1 ℹ Error: 90.77% line coverage does not meet threshold of 95%.
The uncovered lines name themselves: line 17 of app.js is the error handler no test makes fire, 9-10 and 20-21 of the controller the list and create paths no unit test calls. Read the table for which lines are untested, not as a score.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: { node: [22, 24] }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: '${{ matrix.node }}', cache: npm }
- run: npm ci
- run: node --test --experimental-test-coverage --test-coverage-lines=85npm 2,036 ci installs exactly what package-lock.json pins and fails on a stale lock file; npm install would quietly resolve new versions and make the run unreproducible. For a larger project, jest 30.5.1 and vitest 5.0.1 add module mocking, snapshots and parallel workers — and both drive supertest 14,405 unchanged.