Headline: Node.js ships a built-in test runner —
node --testexecutesnode:testfiles with no Jest, no Vitest, and no config file — and paired with type stripping it runs.tstests with zero build. I moved my scripts and small libraries onto it:node:testfor structure,node:assert/strictfor assertions, the built-inmockfor spies and fake timers, and--experimental-test-coveragefor coverage. I kept Vitest for anything that touches the DOM or renders React.
Key takeaways
- The Node.js test runner lives in the built-in
node:testmodule and runs withnode --test; it needs no dependency, no config file, and has been stable since Node.js 20. - Assertions come from the built-in
node:assertmodule — importnode:assert/strictsoassert.equaluses strict (===) comparison. - The
mockobject fromnode:testgives youmock.fn(),mock.method(), andmock.timerswithout a separate mocking library; module mocking viamock.module()is still experimental. - Combined with type stripping,
node --test "src/**/*.test.ts"runs TypeScript tests with no build step on Node.js 22.18+ and Node.js 24. - Coverage is available behind
--experimental-test-coverage, and output format is chosen with--test-reporter(spec,tap,dot,junit,lcov).
Can Node.js run tests without Jest or Vitest?
Yes — Node.js has a built-in test runner, and it needs zero dependencies. The runner is the node:test core module, invoked with node --test; it landed experimentally in Node.js 18 and became stable in Node.js 20. It auto-discovers test files, runs each in its own child process for isolation, and reports in a TAP-based format.
// math.test.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { add } from './math.ts';
test('add sums two numbers', () => {
assert.equal(add(2, 3), 5);
});
Enter fullscreen mode Exit fullscreen mode
Run it with node --test. No jest.config.js, no transform, no ts-jest.
How do I structure tests with node:test?
node:test exports test, plus describe/it and the hooks before, after, beforeEach, afterEach.
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
describe('Cart', () => {
let cart: Cart;
beforeEach(() => { cart = new Cart(); });
it('starts empty', () => assert.equal(cart.total(), 0));
it('sums item prices', () => {
cart.add({ price: 10 });
cart.add({ price: 5 });
assert.equal(cart.total(), 15);
});
});
Enter fullscreen mode Exit fullscreen mode
Focus one test with { only: true } and --test-only, or filter with --test-name-pattern.
How do I mock functions and timers?
Mocking is built in through the mock object — no jest.fn to install.
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
test('notifies the customer once', () => {
const notify = mock.fn();
placeOrder({ items: [{ price: 10 }], notify });
assert.equal(notify.mock.callCount(), 1);
});
Enter fullscreen mode Exit fullscreen mode
mock.method(obj, 'name') spies on a real method, and mock.timers.enable({ apis: ['setTimeout'] }) plus mock.timers.tick(1000) gives deterministic fake timers. Full module mocking via mock.module() is still experimental, so I favor dependency injection instead.
Can I run TypeScript tests without a build step?
Yes, on Node.js 22.18+ and Node.js 24, because Node.js strips type annotations at load time.
node --test "src/**/*.test.ts"
Enter fullscreen mode Exit fullscreen mode
Relative imports need the explicit .ts extension, and Node.js never type-checks — run tsc --noEmit as a separate CI gate. This removed ts-jest and the Vitest transform from every script and library repo I own.
How do I get coverage and CI reports?
Watch mode is node --test --watch. Coverage is a flag: node --test --experimental-test-coverage prints a per-file table. Reporters are chosen with --test-reporter, and you can emit several at once:
node --test \
--test-reporter=spec \
--test-reporter=junit --test-reporter-destination=junit.xml \
--experimental-test-coverage
Enter fullscreen mode Exit fullscreen mode
Thresholds like --test-coverage-lines=80 (Node.js 22+) fail the run below a percentage — enough to gate a PR without a coverage service.
When should I keep Vitest or Jest?
Keep them whenever a test needs a browser-like environment or a build transform the runtime lacks.
Need Node.js test runner Vitest / Jest Dependencies Zero (built in) A framework + transform DOM / React No jsdom, no JSX jsdom/happy-dom + JSX TypeScript Native via type stripping esbuild/SWC transform Snapshots Basic, recent Mature Best for Scripts, libraries, backend Frontend, components, big suitesBackend and library code gets the built-in runner; anything that renders a component stays on Vitest.
FAQ
Q: Is the Node.js test runner production-ready?
A: Yes — the runner and node:test API are stable since Node.js 20. Only module mocking and coverage remain behind experimental flags.
Q: Do I need a config file?
A: No. node --test auto-discovers test files and takes options as CLI flags.
Q: Which assertion library does it use?
A: The built-in node:assert; import node:assert/strict for strict equality. Third-party assertion libraries still work.
Q: Can it test React components?
A: Not well — no DOM, no JSX transform. Use Vitest/Jest with jsdom for components.
Q: How does it mock ES modules?
A: Via mock.module(), still experimental. I prefer dependency injection for stable code.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.
답글 남기기