Skip to content
Latchkey

Vitest vs node:test: An Honest 2026 Comparison

Most comparisons of these two are out of date. node:test is not the minimal runner it was at launch: it has stable mocking, stable snapshots, coverage, watch mode, and test tags. The real gap is narrower and more specific than "features vs zero dependencies".

The usual framing is that Vitest is the full-featured framework and node:test is the bare-bones built-in you settle for when you want zero dependencies. That was accurate in 2023. It is not accurate now, and choosing on it will lead you to add a dependency you may not need.

As of Node 20 the built-in runner is marked Stable, not experimental. It has full mocking of functions, methods, properties, getters, setters, modules, and timers. Snapshot testing stabilised in v22.3.0. It ships spec, tap, dot, junit, and lcov reporters, configurable concurrency, watch mode, test filtering, randomised ordering with a replayable seed, and a rerun-failures flag. Node 26.2.0 added test tags.

Vitest is still meaningfully ahead, but on a shorter and much more specific list than "features". This page compares what each one actually does today, then gives you a decision rule based on what you are testing rather than how many features are on a marketing page.

What each one actually supports in 2026

Capabilitynode:testVitest
StabilityStable since Node 20Stable, current major v4
DependenciesNone, ships with NodeVitest plus Vite
MockingFunctions, methods, properties, getters/setters, modules, timersJest-compatible mocking utilities
SnapshotsStable since v22.3.0Jest-compatible snapshots
CoverageV8-based, still flagged experimentalv8 or Istanbul providers
Watch modeYes (experimental flag)Yes, HMR-style rerun
TypeScriptNative type strippingOut of the box via Vite
JSX and component testsNoVue, React, Svelte and others
Browser modeNoYes
CI reportersspec, tap, dot, junit, lcov built inMultiple, plus sharding for distributed CI
BenchmarkingNoYes, via Tinybench
Type testingNoYes, via expect-type
Test tagsYes, since Node 26.2.0Via filtering

What node:test can do that most comparisons say it cannot

If your mental model of the built-in runner is "test, subtests, and node:assert", it is roughly three years stale. The current surface is substantial.

  • --test-reporter=junit and =lcov are built in, so CI test reporting and coverage upload need no extra packages.
  • --test-concurrency=N controls parallel test files; concurrency is not an afterthought.
  • --test-randomize with --test-random-seed makes order-dependence reproducible instead of mysterious.
  • --test-rerun-failures <state-file> reruns only what failed, which is a meaningful CI optimisation.
  • --experimental-test-coverage produces V8 coverage without instrumenting your build.
node:test, current capabilities
import { test, describe, it, mock, snapshot } from 'node:test';
import assert from 'node:assert';

// module mocking, not just function mocking
mock.module('./payments.js', {
  namedExports: { charge: mock.fn(() => ({ ok: true })) },
});

// timer mocking
mock.timers.enable({ apis: ['setTimeout', 'Date'] });

describe('checkout', () => {
  it('charges once', async (t) => {
    const spy = t.mock.method(cart, 'total');
    await checkout(cart);
    assert.strictEqual(spy.mock.callCount(), 1);
  });
});

What Vitest still does that node:test does not

The remaining gap is real, and it is almost entirely about the browser and the build pipeline rather than about test-running itself.

  • Component testing. Vue, React, Svelte and other component tests need a transform pipeline and a DOM. Vitest inherits both from Vite; node:test has neither.
  • Browser mode. Running the suite in a real browser rather than a simulated DOM has no built-in equivalent.
  • JSX and framework transforms. Vitest reuses your existing Vite config, resolvers, and plugins, so tests see the same module graph as your app. Reproducing that under node:test means building it yourself.
  • Benchmarking and type testing. Tinybench-backed benchmarks and expect-type assertions have no built-in counterpart.
  • Sharding. Vitest supports splitting a suite across CI machines natively, which matters once a suite is long enough to need it.
  • Jest compatibility. The Jest-compatible expect and mocking API makes migrating an existing Jest suite mostly mechanical.

The decision rule: what are you testing?

The honest split is not features against dependencies. It is whether your tests need a browser-shaped environment and a build transform.

What you are testingPickWhy
A published npm librarynode:testZero dependencies in your test path, and nothing to keep on a release cadence
A backend service or CLInode:testNo DOM, no transforms; the built-in surface covers it
A React, Vue, or Svelte appVitestComponent tests need the Vite transform pipeline and a DOM
Anything already on ViteVitestTest config is your existing app config; no second module graph to maintain
A Jest suite you want to moveVitestJest-compatible expect and mocks make it mostly mechanical
A suite that needs benchmarks or type testsVitestNo built-in equivalent exists
A monorepo with bothBothRunner choice is per package; there is no rule against mixing

Startup cost and CI time

The practical argument for the built-in runner in CI is that there is nothing to install before you can run tests. On a cold CI job, npm ci for a test framework and its transform chain is real wall-clock time on every run, and it is time spent before a single assertion executes.

  • node:test adds nothing to install and nothing to resolve; node --test runs against the Node already on the runner.
  • Vitest pulls Vitest plus Vite plus their transitive dependencies, and warms a transform pipeline on first run.
  • Against that, Vitest sharding can cut total wall-clock on a large suite far more than install time costs you.

Running each one in CI

GitHub Actions steps
# node:test - nothing to install
- run: node --test --experimental-test-coverage \
    --test-reporter=junit --test-reporter-destination=junit.xml \
    --test-reporter=lcov --test-reporter-destination=lcov.info

# Vitest
- run: npm ci
- run: npx vitest run --coverage --reporter=junit --outputFile=junit.xml

# Vitest, sharded across 4 machines
- run: npx vitest run --shard=${{ matrix.shard }}/4

Can you migrate between them?

Partly, and the direction matters. Moving from Vitest to node:test is mechanical for plain unit tests: the describe/it shape is the same, and the main work is rewriting expect(...) assertions to node:assert and remapping mocking calls. Moving component tests is not a rewrite, it is a rebuild, because the transform pipeline and DOM have no built-in equivalent.

  • Assertions: expect(a).toBe(b) becomes assert.strictEqual(a, b). Mostly find-and-replace, with care around deep equality.
  • Mocks: vi.fn() becomes mock.fn(); vi.mock() becomes mock.module(), whose API is not identical.
  • Snapshots: both have them, but the file formats differ, so expect to regenerate rather than port.
  • Component tests: no path. These stay on Vitest.

The switching cost is mostly in the parts nobody lists

  • Assertions and mocks usually port mechanically when the target implements a compatible API; custom transformers and framework plugins do not.
  • Snapshot formats differ between runners, so plan to regenerate and review rather than port.
  • Run both suites in parallel in CI for a period and diff the results. A migration that changes which tests fail is not a migration, it is a regression you have not found yet.
  • Coverage numbers move on a runner change even when the tests do not, because instrumentation differs. Re-baseline any coverage gate deliberately.

The verdict

If you are writing a library, a backend service, or a CLI, start with node:test. In 2026 it covers mocking, snapshots, coverage, watch, concurrency, and CI reporters, and it costs you nothing to install and nothing to maintain. The old reason to skip it no longer holds.

If you are testing components, need a browser or DOM, are already on Vite, or are migrating an existing Jest suite, use Vitest. Its advantage is genuine but specific: it owns the build pipeline and the browser, which is exactly what the built-in runner does not attempt.

The one constraint worth checking before committing to node:test is coverage, which remains behind --experimental-test-coverage. If experimental flags are not allowed in your CI, that decides it on its own.

Frequently asked questions

Is the Node built-in test runner production ready?
Yes. node:test has been marked Stable since Node 20, not experimental. Snapshot testing stabilised in v22.3.0 and global setup and teardown landed in v24. The one part still behind an experimental flag is code coverage, via --experimental-test-coverage.
Does node:test support mocking?
Yes, and more thoroughly than most comparisons suggest. It mocks functions, methods, properties, getters and setters, and whole modules via mock.module(), plus timers including setTimeout, setInterval, and Date through mock.timers.
Can node:test run TypeScript?
Yes. Node strips types natively, and passing --no-strip-types extends the default test-file patterns to include .ts, .mts, and .cts. You do not need a separate transform step for straightforward TypeScript, though anything relying on Vite plugins or path aliases still needs configuration.
Is Vitest faster than node:test?
It depends on which part you measure. node:test avoids installing and resolving a framework, so on small suites it usually wins total CI wall-clock. Vitest supports sharding across machines, which wins decisively once a suite is large. Measure your install-versus-execute split rather than assuming.
Can I use node:test for React component tests?
Not practically. Component testing needs a DOM and a JSX transform pipeline, and node:test provides neither. Vitest inherits both from Vite. If you test components, that alone decides the comparison.
Does node:test work with CI test reporting and coverage tools?
Yes. It ships junit and lcov reporters, so JUnit XML for test reporting and lcov for coverage upload both work with no additional packages. That removes what used to be the strongest CI argument against it.
How do I migrate from Vitest to node:test?
For plain unit tests it is mostly mechanical: the describe/it structure carries over, expect(...) assertions become node:assert calls, and vi.fn()/vi.mock() become mock.fn()/mock.module(). Snapshot formats differ so plan to regenerate them. Component tests do not migrate and should stay on Vitest.
Should a library author use Vitest or node:test?
node:test, in most cases. A published library benefits from having nothing in its test path to keep current, no transform chain to debug against consumer setups, and no framework release cadence to track. The built-in runner now covers what library test suites typically need.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card