This article is published in English.
Practical notes: Node’s Native Test Runner vs Jest vs Vitest: One Suite, Three
Operable walkthrough of Practical notes: Node’s Native Test Runner vs Jest vs Vitest: One Suite, Three: contracts, checks, and drop-in code slots for teams shipping this pattern.
Use this as an operator-facing rebuild of the ideas in “Node’s Native Test Runner vs Jest vs Vitest: One Suite, Three Runners, Real Timings”: clear stages, ordered code slots, and recovery notes that survive a handoff. The Overview stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
The three runners, honestly
For the The three runners honestly stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Colocate state with the component that owns the mutation. Lifting everything to a global store makes timing bugs harder to see.
1. node:test, the boring choice that just works
For the 1 node test the stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Colocate state with the component that owns the mutation. Lifting everything to a global store makes timing bugs harder to see.
// node-test/test/string-utils.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { slugify } from '../../src/string-utils.js';
describe('slugify', () => {
it('converts a basic sentence', () => {
assert.equal(slugify('Hello World'), 'hello-world');
});
it('strips diacritics', () => {
assert.equal(slugify('Café résumé'), 'cafe-resume');
});
it('throws TypeError on non-string input', () => {
assert.throws(() => slugify(123), TypeError);
});
});
# Run it, no install step
node --test node-test/test/*.test.js
# Watch mode
node --test --watch node-test/test/*.test.js
# Coverage (still experimental, but the numbers are real V8 counts)
node --test --experimental-test-coverage \
--test-coverage-include='src/**/*.js' \
node-test/test/*.test.js
2. Jest, the default you’ve been carrying
For the 2 Jest the default stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Colocate state with the component that owns the mutation. Lifting everything to a global store makes timing bugs harder to see. For the 2 Jest the default stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
// jest/test/string-utils.test.js
import { slugify } from '../../src/string-utils.js';
describe('slugify', () => {
it('converts a basic sentence', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
it('strips diacritics', () => {
expect(slugify('Café résumé')).toBe('cafe-resume');
});
it('throws TypeError on non-string input', () => {
expect(() => slugify(123)).toThrow(TypeError);
});
});
// jest/jest.config.js, minimal. Run it with NODE_OPTIONS=--experimental-vm-modules
export default {
rootDir: '.',
testMatch: ['<rootDir>/test/**/*.test.js'],
testEnvironment: 'node',
verbose: true,
};
# Run (the flag is required for native ESM)
NODE_OPTIONS=--experimental-vm-modules npx jest --config jest/jest.config.js
# Watch
NODE_OPTIONS=--experimental-vm-modules npx jest --config jest/jest.config.js --watch
3. Vitest, the new default, with one tradeoff
When working through the 3 Vitest the new stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Treat effects as synchronization with the outside world, not as a substitute for derived values during render.
// vitest/test/string-utils.test.js
import { describe, it, expect } from 'vitest';
import { slugify } from '../../src/string-utils.js';
describe('slugify', () => {
it('converts a basic sentence', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
it('strips diacritics', () => {
expect(slugify('Café résumé')).toBe('cafe-resume');
});
it('throws TypeError on non-string input', () => {
expect(() => slugify(123)).toThrow(TypeError);
});
});
// vitest/vitest.config.mjs
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['test/**/*.test.js'],
environment: 'node',
coverage: { provider: 'v8', include: ['../src/**/*.js'] },
},
});
# Run
cd vitest && npx vitest run
# Watch (this is the killer feature)
cd vitest && npx vitest
# Coverage
cd vitest && npx vitest run --coverage
The methodology, so you can call BS
When working through the The methodology so you stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Treat effects as synchronization with the outside world, not as a substitute for derived values during render.
# Reproduce on your own machine
git clone https://github.com/manisuec/techinsights-tutorials
cd techinsights-tutorials/nodejs-test-runner
npm install
bash shared/bench.sh 5 # cold runs
WARM=1 bash shared/bench.sh 5 # warm runs
The real numbers
When working through the The real numbers stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Treat effects as synchronization with the outside world, not as a substitute for derived values during render. When working through the The real numbers stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
=== node:test (Node v24.14.0) [cold] ===
run 1: 91 ms
run 2: 93 ms
run 3: 89 ms
run 4: 90 ms
run 5: 90 ms
median: 90 ms
=== Jest (30.4.1) [cold] ===
run 1: 817 ms
run 2: 689 ms
run 3: 718 ms
run 4: 711 ms
run 5: 685 ms
median: 711 ms
=== Vitest (4.1.11) [cold] ===
run 1: 548 ms
run 2: 648 ms
run 3: 534 ms
run 4: 531 ms
run 5: 535 ms
median: 535 ms
Feature matrix, 2026, last you checked
The Feature matrix 2026 last stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep render work cheap and push expensive derivation behind memoization only after measuring. Premature memo can hide stale props bugs.
When to pick which
The When to pick which stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep render work cheap and push expensive derivation behind memoization only after measuring. Premature memo can hide stale props bugs.
Migrating from Jest to Vitest in 30 minutes
The Migrating from Jest to stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Keep render work cheap and push expensive derivation behind memoization only after measuring. Premature memo can hide stale props bugs. The Migrating from Jest to stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
# 1. Install
npm install -D vitest
# 2. Add a config (or piggyback on vite.config.ts if you have one)
# vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // makes describe/it/expect global
environment: 'jsdom', // or 'node' for backend
setupFiles: ['./tests/setup.ts'],
},
});
# 3. Swap imports, find/replace in your editor
# jest.mock → vi.mock
# jest.fn → vi.fn
# jest.spyOn → vi.spyOn
# require('@jest/globals') → require('vitest')
# jest.useFakeTimers() → vi.useFakeTimers()
# 4. Swap npm scripts
# "test": "jest" → "test": "vitest run"
# "test:watch": "jest --watch" → "test:watch": "vitest"
# 5. If you used babel-jest, delete it and babel.config.js
# 6. Run. Fix the 3-5 things that fail. Drink coffee.
What you would not do
For the What you would not stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Colocate state with the component that owns the mutation. Lifting everything to a global store makes timing bugs harder to see.
The verdict
For the The verdict stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Colocate state with the component that owns the mutation. Lifting everything to a global store makes timing bugs harder to see.
Appendix: how to read the numbers in this post
For the Appendix how to read stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Colocate state with the component that owns the mutation. Lifting everything to a global store makes timing bugs harder to see. For the Appendix how to read stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
Operational checklist
When working through the Operational checklist stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest.
Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline.
Treat effects as synchronization with the outside world, not as a substitute for derived values during render.
Pin dependency versions and record the image digest that ran the demo. Reproducibility beats tribal knowledge.
Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
Treat effects as synchronization with the outside world, not as a substitute for derived values during render.
Before promoting the stack, freeze versions, capture a golden transcript for the critical path, and confirm rollback steps. Shared environments need rate limits, tenancy checks, and a clear owner for secret rotation. Prefer boring reliability over clever one-off demos.
Batch note for b78b143fb7a9: keep provider keys out of the repo, set a per-session token ceiling, and store transcripts next to the eval fixtures so later model swaps stay comparable.