TL;DR
For the 2026 “Bun test vs Vitest” decision, use Bun test when your project already runs on Bun and your suite is mostly server-side TypeScript, utilities, API handlers, or package logic. Use Vitest as the safer default for Vite, React, Vue, Svelte, and most Node.js TypeScript apps. Keep Jest when an existing suite depends on Jest-specific mocks, serializers, presets, or plugins.
Bun test still has the strongest raw feedback-loop story because the runner ships inside the Bun runtime and docs position it as a fast, built-in, Jest-compatible runner with TypeScript, mocking, coverage, reporters, and watch mode. Vitest wins the broader app-testing decision because it inherits Vite’s transform pipeline, supports Jest-style APIs, and has mature DOM/component testing paths. Jest is no longer the obvious new-project default, but it remains the compatibility anchor for teams with deep Jest infrastructure.
Benchmark intent answer
| If you searched | Use this answer |
|---|---|
| "bun test vs vitest" | Bun test is the speed-first choice for Bun/runtime and pure TS/server tests; Vitest is the safer app/component testing default. |
| "vitest vs bun test" | Pick Vitest when Vite, jsdom/browser mode, React Testing Library, or Jest migration compatibility matters more than the fastest possible runner. |
| "bun vs vitest" | Compare the runtime boundary first: Bun test is tied to Bun; Vitest runs naturally in Node/Vite projects and can sit beside Bun package-management. |
| "vitest vs jest" | This page covers the three-runner benchmark decision. For the narrower version/release comparison, see Vitest 3 vs Jest 30. |
Benchmark caveat: test-runner speed is dominated by transforms, DOM setup, mocks, database containers, test isolation, worker configuration, cache warmth, and CI machine class. Treat the example numbers below as directional; run the same representative subset in your own CI before migrating a mature suite.
This canonical route is the benchmark-focused guide. If you want a shorter speed-only comparison, see Bun Test vs Vitest vs Jest 2026: Speed Compared. If your question is mainly Jest-to-Vitest migration depth, use Vitest vs Jest speed benchmarks. For native Node runner coverage, use Node test vs Vitest vs Jest.
Key Takeaways
- Bun test is best when the app already targets Bun, the suite is mostly pure TypeScript or backend logic, and raw local/CI feedback speed matters more than maximal Jest ecosystem parity.
- Vitest is the pragmatic new-project default for Vite and modern TypeScript apps because it combines fast transforms, Jest-style APIs, Vite config reuse, jsdom/browser testing paths, and strong migration ergonomics.
- Jest remains the safest keep-it-running choice for large existing suites with custom serializers, manual mocks, framework presets, Enzyme-era tests, or CI/reporting integrations already tuned around Jest.
- Adoption has shifted. The npm downloads API reported about 45.1M
vitestdownloads, 44.8Mjestdownloads, and 1.6Mbunpackage downloads for 2026-05-08 through 2026-05-14. Treat package downloads as adoption signals, not active-project counts. - Compatibility beats synthetic speed once DOM and mocks enter the suite. Bun test can be dramatically faster on logic tests, but Vitest/Jest usually require fewer surprises for complex component and mocking-heavy suites.
- Migration order matters. Try Vitest first for Node/Vite/Jest migrations; try Bun test first only for Bun-runtime packages or isolated backend test folders where DOM parity is not the blocker.
The Testing Landscape in 2026
JavaScript testing has reached a maturity inflection point. The question teams were asking in 2022 — "should we write tests?" — has been replaced by "which runner gives us the fastest feedback loop without breaking the suite we already have?" The default answer depends on runtime, framework, and migration risk.
Jest used to dominate this decision by bundling assertions, mocks, coverage, snapshots, and jsdom defaults into one familiar package. That bundle value is still real for established codebases, but it is less differentiated for new TypeScript apps because Vitest now covers much of the same API surface while reusing Vite’s transform and module graph.
The speed gap between Jest and modern alternatives is still the main reason teams evaluate a migration. Jest can be fast when tuned with SWC, sharding, cache strategy, and modern configuration, but many older suites still pay Babel/ts-jest transform cost and CommonJS-era module-registry overhead. Vitest usually reduces that cost first. Bun test removes even more layers when the project already runs inside Bun.
The official Bun documentation currently describes bun test as a built-in, Jest-compatible runner with TypeScript support, lifecycle hooks, mocking, watch mode, snapshots, DOM testing, reporters, and code coverage. That makes Bun test much more than a toy runner. The tradeoff is still ecosystem fit: Vitest and Jest have broader documented patterns for React Testing Library, jsdom, custom serializers, and migration from existing Jest suites.
Downloads
| Package | npm downloads, 2026-05-08..2026-05-14 | What the number means |
|---|---|---|
vitest | 45.1M | New-project and migration momentum; also pulled through many template/tooling installs. |
jest | 44.8M | Huge installed base and framework/plugin inertia; still common in mature apps. |
bun | 1.6M | The npm package is only a proxy for Bun adoption; bun test ships with the Bun runtime. |
Downloads are useful for ecosystem gravity, not for deciding the fastest runner in your repo. The runner choice should come from a representative benchmark plus a compatibility audit of mocks, snapshots, DOM tests, and CI reporters.
Speed Benchmarks
Use this as a benchmark shape, not a universal promise:
Representative suite: 200 test files, 1,500 test cases
TypeScript app code, some mocks, light DOM coverage, warm package cache
Jest with SWC transform:
Full run: 40-50s
Watch mode: 6-10s per representative change
Vitest with Vite/esbuild:
Full run: 10-15s
Watch mode: 1-2s per representative change
Bun test for Bun-compatible logic/API tests:
Full run: 3-6s
Watch mode: sub-second to ~1s per representative change
Small projects (< 50 tests): difference is often marginal.
DOM-heavy projects: environment setup can erase much of the raw-runner gap.
The most honest migration benchmark is a two-folder trial: run pure logic/API tests in each candidate runner, then run the component/DOM-heavy tests separately. That split usually reveals whether Bun test should own a fast backend subset while Vitest remains the app/component runner.
Vitest: The Recommended Default
npm install -D vitest @vitest/coverage-v8
# For React:
npm install -D @testing-library/react @testing-library/user-event jsdom
// vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // describe, it, expect without import
environment: 'jsdom', // For DOM/React tests
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: { global: { lines: 80 } },
},
// Concurrent test execution:
pool: 'forks', // Isolate test files
poolOptions: {
forks: { singleFork: false },
},
},
});
// src/test/setup.ts:
import '@testing-library/jest-dom'; // Adds toBeInTheDocument etc.
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(() => cleanup());
// user.test.ts — Vitest test:
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
const mockUser = { id: '1', name: 'Alice', email: 'alice@example.com' };
it('displays user information', () => {
render(<UserProfile user={mockUser} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
it('calls onUpdate when editing', async () => {
const onUpdate = vi.fn();
render(<UserProfile user={mockUser} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('button', { name: 'Edit' }));
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Bob' } });
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(onUpdate).toHaveBeenCalledWith({ ...mockUser, name: 'Bob' });
});
});
Vitest vs Jest API Compatibility
// Vitest has full Jest API compatibility:
// ✅ describe, it, test, expect
// ✅ beforeAll, afterAll, beforeEach, afterEach
// ✅ vi.fn() = jest.fn()
// ✅ vi.mock() = jest.mock()
// ✅ vi.spyOn() = jest.spyOn()
// ✅ vi.useFakeTimers() = jest.useFakeTimers()
// ✅ expect.objectContaining, toMatchSnapshot, etc.
// Migration from Jest:
// 1. Replace jest.fn() → vi.fn() (or use globals)
// 2. Replace jest.mock() → vi.mock()
// 3. Remove babel-jest transform
// 4. Config migration (vitest.config.ts instead of jest.config.js)
Vitest's shared configuration with Vite is a significant practical advantage that benchmarks don't capture. If your project uses Vite for the application build, vitest.config.ts extends vite.config.ts — path aliases, environment variables, plugins, and module resolution configured for your app work identically in tests. With Jest, you'd maintain a separate moduleNameMapper configuration that mirrors your Vite aliases. Configuration drift between application and test config is a common source of subtle test failures that don't reproduce in development.
The watch mode performance difference is where the feedback loop improvement is most felt. Vitest's watch mode uses Vite's module graph to know exactly which test files to re-run when a source file changes. Changing a utility function re-runs only the test files that import it, not the entire suite. Jest's watch mode is file-based — it re-runs tests based on which files changed, without module-level analysis.
Bun Test: Maximum Speed
// Bun uses built-in test runner — same file, no import needed:
import { describe, it, expect, mock, spyOn } from 'bun:test';
describe('UserProfile', () => {
it('renders user name', () => {
// DOM testing with Bun requires happy-dom:
document.body.innerHTML = `<div id="user">Alice</div>`;
expect(document.getElementById('user')?.textContent).toBe('Alice');
});
it('mocks a function', () => {
const greet = mock((name: string) => `Hello, ${name}!`);
expect(greet('Alice')).toBe('Hello, Alice!');
expect(greet).toHaveBeenCalledTimes(1);
});
it('works with async', async () => {
const fetchUser = async (id: string) => ({ id, name: 'Alice' });
const user = await fetchUser('1');
expect(user.name).toBe('Alice');
});
});
# Bun test commands:
bun test # Run all tests
bun test --watch # Watch mode
bun test --coverage # Coverage report
bun test src/components # Run specific directory
bun test --only # Run only tests marked with test.only()
bun test --bail # Stop after first failure
Bun Test Limitations
Bun test tradeoffs vs Vitest/Jest (2026):
⚠️ Strongest when tests run against Bun-compatible runtime APIs
⚠️ Jest compatibility is broad but not a guarantee for every plugin, custom serializer, or mock edge case
⚠️ DOM/component testing needs extra validation against your React Testing Library/jsdom expectations
⚠️ CI/reporting integrations may be simpler than a mature Jest/Vitest setup, but less customized
⚠️ Some Node-only modules, loaders, decorators, or transform assumptions can behave differently
Use Bun test for:
✅ Pure TypeScript/JavaScript logic tests
✅ API/server route tests in Bun-compatible code
✅ Package/library tests that avoid browser simulation
✅ Fast backend subsets in a split test pipeline
Use Vitest for:
✅ React/Vue/Svelte component tests
✅ Vite projects that should reuse app aliases/plugins/env behavior
✅ Jest migrations where mocking, snapshots, and DOM parity are the main risk
Bun test's speed advantage is most pronounced for pure logic tests — functions, classes, API handlers, database queries. These run without any DOM simulation overhead, and Bun's native TypeScript execution (no separate compilation step) eliminates the transformation bottleneck entirely. A test suite of 500 pure logic tests that takes 8 seconds on Vitest might run in under 1 second on Bun.
The gap narrows for React component tests. Both Vitest and Bun test require a simulated DOM environment (jsdom or happy-dom). Vitest's jsdom integration is mature and well-tested with the React Testing Library ecosystem. Bun's happy-dom integration is functional but has edge cases, particularly with MutationObserver and certain Web APIs that React relies on.
Jest: Established Compatibility Default
npm install -D jest @jest/globals ts-jest
# Or with SWC (faster Jest):
npm install -D jest @swc/core @swc/jest
// jest.config.js with SWC (fastest Jest):
module.exports = {
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
testEnvironment: 'jsdom',
setupFilesAfterFramework: ['@testing-library/jest-dom'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
};
When to keep Jest:
→ Existing large codebase with complex Jest config
→ Relying on Jest-specific plugins
→ Team expertise in Jest, migration cost not worth it
→ CI setup built around Jest
When to migrate Jest → Vitest:
→ CI times are too slow
→ New features need ESM native support
→ Starting a new project
→ Vite is already your build tool
CI Integration
All three test runners have good CI support, but each has configuration nuances worth knowing.
For GitHub Actions, Vitest's --reporter=github-actions flag formats failed test output as annotations that appear inline on the PR diff — a significant quality-of-life improvement over parsing raw test output. Enable it in CI environments:
- name: Run tests
run: npx vitest run --reporter=github-actions --coverage
env:
CI: true
Bun test's GitHub Actions support is simpler — no special reporter flag needed, and Bun auto-detects CI environments for output formatting. The main CI consideration for Bun is caching the Bun binary itself (it's a single ~80MB binary) and the test dependencies:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Run tests
run: bun test --coverage
Jest with SWC on GitHub Actions benefits from cache configuration for the SWC transform cache. Without caching, SWC re-compiles all TypeScript on every CI run:
- name: Cache SWC
uses: actions/cache@v4
with:
path: .swc
key: swc-${{ hashFiles('package-lock.json') }}
Decision Guide
Use Vitest if:
→ New project that is not committed to Bun as the runtime
→ React/Vue/Svelte components or jsdom/browser-mode tests matter
→ Vite aliases, plugins, or environment loading should be shared with tests
→ You are migrating from Jest and want the lowest compatibility risk
→ You want the best balance of speed, ecosystem, and CI/reporting maturity
Use Bun test if:
→ The app/package already targets Bun in development and production
→ Most tests are pure TypeScript, backend logic, API routes, or package utilities
→ Maximum local feedback speed is the main bottleneck
→ You can keep DOM-heavy tests in Vitest/Jest if compatibility issues appear
Keep Jest if:
→ A large existing suite already works and migration ROI is unclear
→ You rely on Jest plugins, custom serializers, manual mocks, or old framework presets
→ CI, coverage, sharding, and developer workflows are already tuned around Jest
→ Compatibility risk is more expensive than the speed savings
Snapshot Testing: Inline Snapshots and Custom Serializers
Snapshot testing captures the serialized output of a component or function and fails if it changes unexpectedly. All three runners support snapshots but with meaningful differences.
Jest and Vitest Snapshot Compatibility
Vitest uses the same .snap file format as Jest — existing Jest snapshots migrate without regeneration. The toMatchSnapshot() and toMatchInlineSnapshot() APIs are identical:
// Both Jest and Vitest support this syntax
test('renders user card', () => {
const { container } = render(<UserCard name="Alice" role="admin" />);
expect(container).toMatchSnapshot(); // External .snap file
// Inline snapshot — stored directly in test file
expect({ name: 'Alice', active: true }).toMatchInlineSnapshot(`
{
"active": true,
"name": "Alice",
}
`);
});
If your suite relies heavily on inline snapshot update ergonomics or custom snapshot serializers, verify Bun's current support against your exact assertion patterns before migrating. Vitest and Jest remain the safer default for snapshot-heavy React/component suites because their Jest-compatible snapshot workflows are more widely exercised.
Custom Snapshot Serializers
Serializers control how values are converted to snapshot strings. Jest's expect.addSnapshotSerializer() works identically in Vitest:
// Custom serializer for Date objects
expect.addSnapshotSerializer({
test: (val) => val instanceof Date,
print: (val: Date) => `Date<${val.toISOString()}>`,
});
expect(new Date('2026-03-08')).toMatchInlineSnapshot(`
Date<2026-03-08T00:00:00.000Z>
`);
This is particularly valuable for testing API responses containing timestamps — without a serializer, snapshots capture exact millisecond values that differ across test runs. If custom serializers are critical to your suite, verify Bun's current snapshot support against those exact serializer and update flows before moving that folder; otherwise, transform dates and other complex values explicitly before snapshotting.
Migrating from Jest to Vitest in Practice
The API compatibility between Jest and Vitest is high, but a real migration involves more than changing import paths. Here are the common friction points:
Step 1: Replace Config
// jest.config.ts → vitest.config.ts
// After (Vitest)
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
},
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
// CSS handled by Vite's CSS plugin automatically
},
});
The moduleNameMapper for path aliases becomes Vite's resolve.alias — and if your project uses Vite for the app build, this is already configured. Most migrations simplify their module mapping significantly.
Step 2: Replace Jest-Specific APIs
// Jest → Vitest equivalents
jest.fn() → vi.fn()
jest.mock('./module') → vi.mock('./module')
jest.spyOn(obj, 'fn') → vi.spyOn(obj, 'fn')
jest.useFakeTimers() → vi.useFakeTimers()
jest.setSystemTime() → vi.setSystemTime()
jest.clearAllMocks() → vi.clearAllMocks()
jest.resetModules() → vi.resetModules()
The vi.mock() hoisting behavior — where mocks are automatically moved to the top of the file — matches Jest. This maintains compatibility with the common pattern of calling vi.mock() inside test files without wrapping in beforeAll.
Step 3: Handle ESM-Specific Issues
Jest with Babel transforms away ESM syntax. Vitest runs native ESM — a few patterns that "worked" in Jest due to CommonJS semantics may break:
// Circular dependencies: worked in Jest (CJS), may fail in Vitest (ESM)
import { handler } from './module-with-circular-deps';
// Fix: restructure to avoid the circular dependency
// Or use vi.mock() to break the cycle
For most applications, this is never encountered. Circular dependencies are a code quality issue that Vitest exposes at import time rather than at test time — a better failure mode.
Monorepo and Large Codebase Considerations
The right test runner changes at different codebase scales.
CI Spend
Test runner speed translates directly to CI minutes. A 10-minute Jest test suite running on every PR, 20 times per day across 5 developers, consumes 200 CI minutes daily. The same suite in Vitest at 3 minutes: 60 minutes. The annual cost difference on GitHub Actions (~$800/year) is meaningful, but the real gain is the developer feedback loop: a test that takes 30 seconds to run gets run after every change; one that takes 5 minutes runs only when you're about to commit.
Monorepo Tooling Integration
Monorepo tooling integration heavily favors Vitest. Turborepo and Nx both have documented Vitest integration paths with caching for test results — a passing test suite cached by Turborepo doesn't re-run on unchanged packages. Bun test's caching integration with Turborepo is functional but less documented. Jest's caching integration with Turborepo requires careful configuration to prevent cache invalidation on every run.
TypeScript Project References
Monorepos with TypeScript project references require test runners that understand the reference graph. Vitest's TypeScript support handles project references via resolve.tsconfig configuration. Jest requires moduleNameMapper entries per package — unmaintainable at 10+ packages.
The Practical Decision for 2026
- Vitest: Default for new JavaScript/TypeScript projects that are not Bun-runtime-first. Jest-style APIs, TypeScript ergonomics, Vite integration, and stronger DOM/component testing paths make it the safest broad recommendation.
- Bun test: Choose when running Bun as the runtime or when you can carve out pure logic/API tests that benefit from Bun’s integrated runner. It can be the fastest lane in a split pipeline.
- Jest: Maintain if you have a large, working Jest codebase with complex configuration. The single highest-impact improvement without full migration is often replacing Babel/ts-jest transforms with SWC (
@swc/jest) and tightening sharding/cache behavior.
Official source notes checked for this refresh
- Bun test docs (
https://bun.sh/docs/test, accessed 2026-05-15): built-in test runner, TypeScript support, lifecycle hooks, mocks, snapshots, DOM testing, reporters, and coverage are documented in the current testing guide. - Vitest guide (
https://vitest.dev/guide/, accessed 2026-05-15): getting-started and guide navigation cover globals, environments, mocking, reporters, coverage, browser mode, and migration topics. - Jest docs (
https://jestjs.io/docs/getting-started, accessed 2026-05-15): current docs identify Jest 30.x and cover install paths, TypeScript setup, matchers, mocks, and setup/teardown. - npm downloads API (
https://api.npmjs.org/downloads/point/last-week/..., accessed 2026-05-15): reportedvitest45,055,558,jest44,758,857, andbun1,615,104 downloads for 2026-05-08 through 2026-05-14.
Frequently Asked Questions
Should I use Bun test or Vitest for a new project?
Use Vitest unless your entire runtime stack is Bun. Vitest has better React Testing Library integration, supports inline snapshots, has broader mocking compatibility with the Jest ecosystem, and provides more consistent jsdom behavior for component testing. Bun test's speed advantage is meaningful for pure TypeScript logic tests but narrows significantly when DOM simulation is involved. For projects where you're not already committed to Bun as a runtime, Vitest is the more complete solution.
How difficult is migrating from Jest to Vitest in practice?
Most migrations take hours, not days. The API compatibility is genuine — describe, it, expect, and most matchers work unchanged. The main effort is replacing jest.* namespace calls with vi.* equivalents and updating the configuration file from jest.config.js to vitest.config.ts. Projects with complex Jest configuration (manual mocks in __mocks__/, extensive moduleNameMapper entries, custom transformers) require more care but are still tractable. The single highest-risk area is module mocking: vi.mock() hoisting behaves like Jest's but with subtle differences in how it interacts with ES module dynamic imports.
What about Deno's test runner?
Deno has a built-in test runner (deno test) similar in concept to Bun test — integrated, zero-config, fast. For projects already committed to Deno, it's the obvious choice. For projects on Node.js or Bun, Deno's test runner isn't relevant. The JavaScript runtime fragmentation (Node.js, Deno, Bun) affects test runner choice at the margin — each runtime's built-in test runner is fastest for that runtime's native APIs — but the majority of application code and testing patterns are portable across all three.
Is Bun test stable enough for production CI?
Yes, for the right slice of the suite. Bun test is stable enough for pure TypeScript/JavaScript tests, backend services, API handlers, package logic, and Bun-runtime projects. For React component testing or suites that depend heavily on Jest/jsdom edge behavior, validate a representative subset before moving CI. Many teams get the best result from a split pipeline: Bun test for backend/logic folders and Vitest for component/browser-facing tests.
Compare Vitest, Jest, and Bun download trends on PkgPulse. Related: Best JavaScript Testing Frameworks 2026 and Best JavaScript Runtimes 2026.
