Skip to main content

node:test vs Vitest vs Jest: Should You Use Node's Built-In Test Runner in 2026?

·PkgPulse Team

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:test is 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:test lacks: 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:

RunnerCold startRe-run (watch)Parallel
Vitest0.8s0.1s✅ Native
Jest3.2s0.4s✅ Workers
node:test0.3sN/A--test-concurrency
node:test + tsx0.6sN/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

Featurenode:testVitestJest
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
Dependencies0~15MB~50MB
Weekly downloadsN/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-types continues 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-types on various TS patterns

See Vitest vs Jest download trends on PkgPulse — real-time npm data.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.