Guide
Vitest vs Jest vs node:test vs Bun Test



Compare Vitest, Jest, node:test and Bun test for JavaScript unit tests: TypeScript, mocks, snapshots, coverage, runtime compatibility and a repeatable CI trial.
Contents
TL;DR
Choose Vitest for a Vite or TypeScript application that benefits from shared transforms and Jest-style assertions. Keep Jest when its presets, mocks and serializers already support a reliable suite. Choose node:test for Node-focused packages that can use Node assertions, and Bun test for code intended to run on Bun. Decide with representative tests, coverage and CI behavior; none of these choices guarantees a universal speed improvement.
Quick decision matrix
| Runner | Best starting point | Main cost to check |
|---|---|---|
| Vitest 5 | Vite applications and TypeScript unit suites | Mock semantics, plugins and existing test environments |
| Jest 30 | Established suites with working presets and integrations | ESM setup and TypeScript transforms |
| node:test | Node libraries, utilities and service logic | Assertion and module-mocking migration |
| Bun test | Bun applications and Bun-compatible backend tests | Runtime differences and incomplete Jest API compatibility |
The released package baselines are Vitest 5.1.0, Jest 30.5.2, Node.js 26.13.0 and Bun 1.4.4. The native runners ship with their runtime; they are not separate npm test-framework dependencies. Check the Vitest registry record, Jest registry record, Node record and Bun record when pinning a trial.
This matrix is an editorial decision aid based on documented behavior, not a performance ranking. Package download counts would not establish the number of teams using a runner, and downloads of the Bun npm package would omit other installation methods. Use the Vitest vs Jest package comparison for package-health context alongside the compatibility checks here.
node:test is a viable Node-native option
The Node test runner documentation describes suites, hooks, asynchronous tests, mocks, reporters, watch mode, coverage and snapshot testing. Claims that the runner simply lacks watch mode or snapshots are outdated. Some individual APIs and flags have their own stability status; evaluate the version deployed in your CI rather than assuming every feature shares the runner's overall status.
A small Node library can start with explicit imports and no framework installation:
// sum.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
test('adds two values', () => {
assert.equal(2 + 3, 5);
});
node --test sum.test.mjs
node --test --watch sum.test.mjs
node --test --experimental-test-coverage sum.test.mjs
This simplicity matters when the package already targets Node APIs. You avoid a separate assertion framework unless you choose to add one. It also keeps the test runtime close to the production runtime. A dependency-free test harness still needs test discovery, fixtures, cleanup and reporting conventions that the team can maintain.
Moving a large Jest suite to Node assertions is a different proposition. assert.deepEqual and expect(...).toEqual(...) are separate interfaces. Tests relying on custom matchers, automatic module mocks or framework presets need deliberate translation. Start with pure utilities, then test dependencies that load state at import time. Check the current documentation for module-mocking flags and limitations before adopting them across a package.
Native TypeScript execution also does not imply type checking or support for every compiler transformation. Keep a separate type-checking step and check your runtime's supported syntax. A successful unit-test run cannot prove that declarations exported to consumers are correct.
Vitest fits shared application tooling
Vitest's getting-started guide explains its relationship with Vite configuration and its test CLI. Reusing aliases and application plugins can reduce duplicated configuration. That is useful when application imports depend on transformations that a second test pipeline would otherwise need to reproduce.
A minimal backend configuration can keep imports explicit:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});
import { expect, test } from 'vitest';
test('adds two values', () => {
expect(2 + 3).toBe(5);
});
Use vitest run for a finite CI execution and vitest for the interactive development workflow. Reusing Vite does not eliminate environment differences: a Node environment has no document, and simulated DOM environments do not implement every browser behavior. Install and configure the environment required by the tests. For Testing Library matchers, use the Vitest-specific integration rather than assuming Jest globals exist.
For a monorepo, define test projects around real differences such as Node versus DOM environments or package-specific plugins. Keep discovery patterns disjoint so a test does not run twice. Audit shared setup for expensive work before increasing workers. More workers can compete for database connections and memory; tuning isolation or concurrency changes test behavior and needs its own verification.
The optional UI and coverage integrations are conveniences, not reasons to discard a functioning runner. Evaluate whether they help the team diagnose actual failures. Browser Mode and full browser automation have separate requirements, covered in the browser component-testing comparison.
Jest remains useful when the suite depends on it
Jest's setup documentation covers configuration and TypeScript integration. Preserve a working Jest suite when framework presets, custom reporters, manual mocks or snapshot serializers carry behavior you would otherwise need to rebuild. A framework's supported testing recipe is stronger evidence than a generic recommendation to switch runners.
Jest's ESM documentation still labels ESM support experimental. ESM execution and module mocking differ from CommonJS: static imports are evaluated before ordinary test code, so a CommonJS mocking recipe cannot be copied blindly. Check transform output, package module type and the documented launch requirements together.
TypeScript compilation and type checking remain separate decisions. If a transform strips types, keep the type checker elsewhere in CI. If the existing pipeline checks types while transforming tests, compare equivalent work before attributing a timing difference to the runner.
Keeping Jest does not mean freezing configuration indefinitely. Remove obsolete transforms only after proving equivalent behavior, update supported environments and inspect the longest test files. A project can gain more from removing repeated network setup than from changing its test API. For framework-specific suites, validate the actual preset rather than assuming a replacement supports it.
Bun test changes the execution runtime
Bun's test documentation describes a built-in runner with TypeScript, JSX, hooks, watch mode, snapshots and mocking. It aims for Jest compatibility, while explicitly warning that compatibility is incomplete. Installing dependencies with Bun does not require using its runner; those are separate choices.
// sum.test.ts
import { expect, test } from 'bun:test';
test('adds two values', () => {
expect(2 + 3).toBe(5);
});
bun test
bun test --watch
bun test --coverage
A Bun application benefits from testing against the same runtime it will ship. A Node application tested only on Bun can miss runtime-specific failures. If Node remains a supported target, retain a Node execution lane for relevant package behavior, including native dependencies and module loading.
DOM testing needs an environment and setup; a Jest-compatible assertion API does not supply a browser document. Trial custom serializers, timers, module mocks and reporters independently. If only the backend utility subset works unchanged, keep that subset explicit rather than treating partial compatibility as proof that the entire suite migrated.
For CI, pin the Bun version and install dependencies from the lockfile. Record the exact runner command. bun test and bun run test can execute different things: the former invokes Bun's runner, while the latter runs the package script. That distinction prevents an accidental runtime switch while evaluating installation speed.
Mocking, snapshots and coverage need parity checks
The Vitest migration guide documents differences from Jest. Replacing the word jest with vi is only the beginning. Review mock factories, import order, manual mocks, mock reset behavior and timer cleanup. Static imports and hoisted mocking calls make module state particularly sensitive to changes in the test loader.
For globals, either import test APIs explicitly or configure the runner and TypeScript types together. Removing Jest global types before every package has migrated can break packages still running Jest. Keep the old runner available until the trial passes so you can compare failures without reconstructing the previous setup.
Snapshot migrations require review. Regenerated snapshots can contain harmless formatting changes or real behavior regressions. Check custom serializers and inline snapshots separately, and do not approve an entire snapshot update merely because the new runner produced it. Test a known failure to confirm the replacement still detects it.
Vitest coverage supports V8 and Istanbul providers. Bun coverage has its own options, while Node exposes experimental coverage flags. Match source inclusion, exclusions and thresholds before comparing reports. Different reports can count different files; a higher percentage is not automatically better coverage.
Reproducible benchmark methodology
This guide publishes no comparative timing results. To answer a speed question, measure your suite with a recorded source commit, lockfile, runtime versions, operating system, CPU allocation and exact commands. Keep coverage off for the first execution comparison, then measure it separately with equivalent source scope. Include transformation and environment setup in total elapsed time.
Use two representative groups: pure logic or API tests, and DOM or heavily mocked tests. Port assertions without reducing test cases, skipping failures or removing expensive setup from only one candidate. Confirm the same number of intended tests runs and that deliberately broken application behavior fails both versions. A runner that silently discovers fewer tests is not a performance improvement.
| Trial | Hold constant | Record |
|---|---|---|
| Clean full run | Source, dependencies, CPU and test scope | Wall time, failures and peak memory |
| Repeated full run | Same scope and cache policy | Distribution across repeated executions |
| Watch edit | Same source change and affected tests | Time to an accurate pass or failure |
| Coverage run | Same source inclusions and thresholds | Wall time and uncovered files |
| CI run | Same machine class and services | Whole job duration and artifacts |
Publish every repetition, not just the fastest result. Separate dependency installation from test execution and state whether caches are warm. Record flakiness, skipped tests and failures beside timing. Do not generalize a utility benchmark to component tests or compare an in-band baseline with a parallel candidate without explaining the difference.
For a monorepo, first measure one package, then the entire dependency graph. Sharding and worker settings should respect service limits and memory. Compare total CI time too: faster test execution may leave installation, database startup or artifact upload unchanged. Keep a rollback command and acceptance criteria in the trial PR.
When to use which
Choose node:test when the code targets Node, Node assertions are sufficient and maintaining a small harness is preferable to adding a framework. Trial mocks and coverage on the minimum Node version the package supports.
Choose Vitest when application transforms, TypeScript and Vite configuration are central to the suite. Verify DOM setup and Jest migration differences before replacing the default test script.
Keep Jest when the current preset and integrations work and the measured benefit of migration does not cover the compatibility work. A reliable test suite has value even when another runner wins a synthetic timing test.
Choose Bun test when Bun is the intended runtime or a clearly scoped compatible test target. Preserve runtime coverage for every platform you promise to support. Use browser automation separately for navigation, layout and browser integration; passing unit tests does not establish those behaviors.
Source notes
The official documentation linked above supports each runner's own capabilities. The npm records establish released package versions, not comparative performance. The decision and trial matrices are original editorial guidance. There are no measured speed multipliers, active-project counts or anonymous migration case studies in this comparison.
Related guides
- Jest-to-Vitest migration walkthrough for the step-by-step conversion task.
- Vitest Browser Mode vs Playwright component testing for real-browser component checks.
- Node.js vs Bun vs Deno for runtime compatibility and deployment trials.
- tinybench vs mitata vs vitest bench for benchmarking library code rather than test-runner throughput.