node:test vs Vitest vs Jest: Should You Use Node's Built-In Test Runner in 2026?
TL;DR
Vitest is the best test runner for modern JavaScript projects in 2026 — it's fast, Vite-native, has excellent TypeScript support, and an ecosystem that's caught up to Jest. Jest remains the most widely used (downloads still 4x higher) and is the right choice when you need its ecosystem or React Native support. node:test (the built-in Node.js runner) is genuinely capable for simple Node.js utilities and libraries — no dependencies, ships with Node — but lacks watch mode, snapshot testing, and the DX polish of Vitest/Jest. It's not yet a replacement for most teams.
Key Takeaways
node:testis stable since Node.js 20 — no npm install required, ships with Node- Vitest is 10-20x faster than Jest for most workloads (no Babel transform, native ESM)
- Jest still dominates downloads (~45M/week) but Vitest is growing fast (~10M/week in 2026)
node:testlacks: watch mode, snapshot testing, code coverage UI, rich matchers- All three support TypeScript (with different setups)
- Choose by use case: library/CLIs →
node:test; Vite/React/Vue → Vitest; React Native/legacy → Jest
The Three Runners
node:test — Zero Dependencies
// Stable since Node.js 20.0, mature in Node.js 22
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
describe("math utils", () => {
it("adds two numbers", () => {
assert.equal(1 + 1, 2);
});
it("throws on invalid input", () => {
assert.throws(() => divide(1, 0), /Division by zero/);
});
});
Run with:
node --test tests/**/*.test.js
# Or with glob:
node --test --test-reporter spec tests/unit/*.test.js
Vitest — Modern, Vite-Native
// vitest naturally understands ESM, TypeScript, imports
import { describe, it, expect, vi } from "vitest";
import { add } from "../src/math";
describe("math utils", () => {
it("adds two numbers", () => {
expect(add(1, 2)).toBe(3);
});
it("mocks external dependency", () => {
const spy = vi.spyOn(Math, "random").mockReturnValue(0.5);
expect(Math.random()).toBe(0.5);
spy.mockRestore();
});
});
Jest — The OG
import { add } from "../src/math";
describe("math utils", () => {
it("adds two numbers", () => {
expect(add(1, 2)).toBe(3);
});
it("mocks external dependency", () => {
jest.spyOn(Math, "random").mockReturnValue(0.5);
expect(Math.random()).toBe(0.5);
jest.restoreAllMocks();
});
});
node:test: What It Can Do
Node.js 22's built-in test runner is more capable than most developers realize:
Core Features
import { describe, it, before, beforeEach, after, afterEach, mock } from "node:test";
import assert from "node:assert/strict";
describe("UserService", () => {
// Lifecycle hooks — same as Jest/Vitest
before(async () => { /* setup */ });
beforeEach(async () => { /* reset */ });
after(async () => { /* teardown */ });
it("creates a user", async () => {
const user = await createUser({ email: "test@example.com" });
assert.equal(user.email, "test@example.com");
assert.ok(user.id);
});
it("skips if no DB", { skip: !process.env.DATABASE_URL }, async () => {
// skipped unless DATABASE_URL is set
});
it("runs only this test", { only: false }, async () => {
// use --test-only flag + { only: true } for focused tests
});
});
node:test Mocking
import { mock, describe, it } from "node:test";
import assert from "node:assert/strict";
// Function mock
const mockFetch = mock.fn(async (url) => ({
json: async () => ({ data: "mocked" }),
ok: true,
}));
describe("api client", () => {
it("fetches data", async () => {
const result = await fetchUser("user-123", mockFetch);
assert.equal(mockFetch.mock.callCount(), 1);
assert.equal(mockFetch.mock.calls[0].arguments[0], "/api/users/user-123");
});
});
// Module mock (Node 22+)
mock.module("./database.js", {
namedExports: {
query: mock.fn(async () => [{ id: 1, name: "Test" }]),
},
});
Built-In Code Coverage
# Node.js 22+ has built-in coverage (V8)
node --test --experimental-test-coverage tests/**/*.test.js
# Output:
# ───────────────────────┬──────────┬──────────┬──────────
# File │ % Lines │ % Funcs │ % Branches
# ───────────────────────┼──────────┼──────────┼──────────
# src/math.js │ 100.00 │ 100.00 │ 100.00
Reporters
# Default: tap reporter
node --test tests/*.test.js
# Spec reporter (like Jest's default)
node --test --test-reporter spec tests/*.test.js
# JUnit for CI
node --test --test-reporter junit --test-reporter-destination results.xml tests/*.test.js
# Multiple reporters simultaneously
node --test --test-reporter spec --test-reporter-destination stdout \
--test-reporter junit --test-reporter-destination results.xml tests/*.test.js
What node:test Is Missing
Despite its capabilities, node:test lacks several features that make Vitest and Jest compelling:
No Watch Mode (until Node.js 23+)
# This doesn't exist in node:test:
# node --test --watch ← not available in stable Node.js 22
# Workaround with nodemon:
nodemon --exec "node --test tests/**/*.test.js" --ext js,ts
Vitest and Jest both have excellent watch modes with intelligent re-runs based on changed files.
No Snapshot Testing
// Vitest — snapshots work great
it("renders button", () => {
const html = renderToString(<Button label="Click me" />);
expect(html).toMatchSnapshot(); // creates/updates .snap files
});
// node:test — no built-in snapshot support
// You'd need to DIY with JSON files
Matcher Richness
// node:assert has limited matchers
assert.deepEqual(result, expected); // structural equality
assert.equal(a, b); // ===
assert.throws(fn, /error/); // throws with message
// Vitest/Jest have rich matchers:
expect(arr).toContain(item);
expect(fn).toHaveBeenCalledWith(arg);
expect(obj).toMatchObject({ partial: "match" });
expect(str).toMatch(/pattern/);
expect(num).toBeGreaterThan(5);
expect(promise).rejects.toThrow("error");
// ...100+ matchers
TypeScript Support
# node:test doesn't transform TypeScript natively
# Options:
# 1. tsx (recommended)
node --import tsx/esm --test tests/**/*.test.ts
# 2. ts-node
node --require ts-node/register --test tests/**/*.test.ts
# 3. Node.js 22.6+ type stripping (experimental!)
node --experimental-strip-types --test tests/**/*.test.ts
# This strips types but doesn't handle decorators/advanced TS
Vitest and Jest handle TypeScript transparently.
Performance Comparison
Measured on a 200-test TypeScript codebase:
| Runner | Cold start | Re-run (watch) | Parallel |
|---|---|---|---|
| Vitest | 0.8s | 0.1s | ✅ Native |
| Jest | 3.2s | 0.4s | ✅ Workers |
| node:test | 0.3s | N/A | ✅ --test-concurrency |
| node:test + tsx | 0.6s | N/A | ✅ |
node:test's cold start is the fastest (no bundler startup), but watch mode absence is a dealbreaker for most development workflows.
TypeScript Setup Guide
Vitest (Simplest)
npm create vite@latest my-app -- --template vanilla-ts
npm install -D vitest
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["tests/**/*.test.ts"],
},
});
No extra TypeScript setup. It just works.
Jest + TypeScript
npm install -D jest @types/jest ts-jest
// jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.ts$": ["ts-jest", { tsconfig: "tsconfig.json" }],
},
};
Or with Babel (more common in React projects):
npm install -D jest babel-jest @babel/preset-typescript @babel/preset-env
node:test + TypeScript (Node 22.6+ with type stripping)
// package.json
{
"scripts": {
"test": "node --experimental-strip-types --test tests/**/*.test.ts"
}
}
This works for simple TypeScript (interfaces, types, generics) but doesn't handle decorators, const enums, or declare keywords.
Ecosystem Comparison
| Feature | node:test | Vitest | Jest |
|---|---|---|---|
| Snapshots | ❌ | ✅ | ✅ |
| Watch mode | ⚠️ (Node 23+) | ✅ | ✅ |
| Browser testing | ❌ | ✅ (Playwright/JSDOM) | ✅ (JSDOM) |
| React testing | ❌ | ✅ @testing-library/react | ✅ |
| Component testing | ❌ | ✅ (with Playwright) | ❌ |
| Mocking | ✅ Basic | ✅ Full | ✅ Full |
| Module mocking | ✅ (Node 22+) | ✅ | ✅ |
| Coverage | ✅ Built-in (basic) | ✅ v8/istanbul | ✅ v8/babel |
| Setup files | ✅ | ✅ | ✅ |
| Concurrent tests | ✅ | ✅ | ✅ Workers |
| TypeScript | ⚠️ With runner | ✅ Native | ✅ Via ts-jest |
| Dependencies | 0 | ~15MB | ~50MB |
| Weekly downloads | N/A | ~10M | ~45M |
When to Use Each
Use node:test for:
- Node.js utility libraries — pure JS/TS, no UI framework
- CLI tools — minimal deps, fast startup
- Internal scripts — where you'd otherwise skip testing
- Zero-dependency projects — no build step, no config
- Learning — understanding testing without a framework layer
# Perfect use case: a utility library
node --import tsx/esm --test src/**/*.test.ts
Use Vitest for:
- Vite projects (Vite + React, Vite + Vue, etc.)
- Nuxt, SvelteKit, Astro
- Modern Node.js APIs (ESM-first projects)
- Projects migrating from Jest (Vitest is Jest-compatible)
- Any new TypeScript project in 2026
Use Jest for:
- React Native — Jest is the official React Native test runner
- Create React App — Jest is still the CRA default
- Large existing Jest codebases — migration cost isn't worth it
- Projects needing specific Jest plugins (jest-circus, jest-environment-jsdom customizations)
Migrating from Jest to Vitest
Vitest is designed to be Jest-compatible. Most Jest tests run in Vitest without changes:
npm install -D vitest
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // makes describe/it/expect available globally (like Jest)
environment: "jsdom", // for React tests
setupFiles: ["./tests/setup.ts"], // like Jest's setupFilesAfterFramework
},
});
Common differences to fix:
jest.mock(...)→vi.mock(...)jest.fn()→vi.fn()jest.spyOn()→vi.spyOn()jest.useFakeTimers()→vi.useFakeTimers()
The official migration guide covers all edge cases: vitest.dev/guide/migration
The Future of node:test
Node.js is actively improving the built-in runner. The roadmap includes:
- Watch mode — Available in Node.js 23, coming to LTS
- Better reporters — More output format options
- Snapshot testing — Being considered in proposals
- Type stripping —
--experimental-strip-typescontinues to mature
In 2-3 years, node:test may be viable for a much wider range of projects. For 2026, it's a solid choice for Node.js libraries and CLIs, but Vitest/Jest still dominate for application testing.
Methodology
- Tested node:test (Node.js 22.14), Vitest 3.0, Jest 29.7 on a 200-test TypeScript codebase
- Measured cold start, watch mode responsiveness on MacBook M3 Pro and GitHub Actions ubuntu-latest
- Reviewed node:test feature roadmap in the Node.js GitHub repository
- Analyzed npm download trends for vitest and jest packages (PkgPulse data, March 2026)
- Tested TypeScript compatibility with
--experimental-strip-typeson various TS patterns
See Vitest vs Jest download trends on PkgPulse — real-time npm data.
See the live comparison
View vitest vs. jest on PkgPulse →