Bun vs Vitest vs Jest Benchmarks (2026)
TL;DR
Vitest is the 2026 default for JavaScript/TypeScript testing — Jest-compatible API, fast (esbuild-based), tight Vite integration, and native TypeScript. Bun test is 3-10x faster than Vitest but has lower compatibility with Jest ecosystem (missing some Jest APIs). Jest is legacy but still dominant at 20M+ downloads/week due to inertia and extensive plugin ecosystem. For new projects: Vitest. For Bun projects: Bun test.
Key Takeaways
- Vitest: 4M downloads/week, Jest-compatible API, native TypeScript, Vite integration
- Bun test: Built-in to Bun, 3-10x faster than Vitest, growing Jest compatibility
- Jest: 20M downloads/week, massive ecosystem, slower (Node.js + Babel/SWC transform)
- Speed: Bun test > Vitest > Jest (10-20x Jest slowdown on large codebases)
- Migration: Vitest → Jest: mostly compatible; Bun test → Jest: some gaps
- Coverage: All three support V8/Istanbul coverage
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 test runner gives us the best feedback loop?" The default answer is changing.
Jest's 20M weekly downloads represent a decade of ecosystem inertia. Jest was the right choice for a long time: it bundled everything (assertions, mocking, coverage, snapshot testing) into a single package when the alternatives required assembling Mocha + Chai + Sinon + Istanbul yourself. That bundle value is now less differentiated — Vitest bundles the same capabilities with native ESM and TypeScript.
The speed gap between Jest and modern alternatives is the main driver of migration. Jest's architecture predates native ESM in Node.js, which means it transforms code through Babel or SWC before executing. This transformation step adds wall-clock time to every test run. On a 1,000-test suite, the difference between 45 seconds (Jest) and 12 seconds (Vitest) means the difference between "run tests in CI" and "run tests on every save."
Bun's test runner represents the endpoint of this trend. By building the test runner, JavaScript runtime, package manager, and bundler into a single binary with Zig-optimized internals, Bun eliminates most of the overhead between writing a test and seeing a result. The tradeoff is ecosystem: the bun:test API doesn't cover every Jest use case, and some testing-library integrations have rough edges.
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
jest | ~20M | → Slight decline |
vitest | ~4M | ↑ Fast growing |
bun (includes test) | ~1.5M | ↑ Fast growing |
Speed Benchmarks
Project: 200 test files, 1500 test cases
Jest (SWC transform):
Full run: 45s
Watch mode: 8s per change
Vitest (Vite/esbuild):
Full run: 12s (3.7x faster than Jest)
Watch mode: 1.5s (5x faster than Jest)
Bun test:
Full run: 4s (11x faster than Jest)
Watch mode: 0.5s (16x faster than Jest)
Note: Gains are proportional to project size.
Small projects (< 50 tests): difference is marginal.
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 gaps vs Vitest/Jest (2026):
❌ No jsdom environment (use happy-dom)
❌ Some Jest API differences (minor)
❌ No inline snapshots (snapshots work, inline doesn't)
❌ Limited @testing-library/react support
❌ No @vitest/coverage-v8 equivalent (has built-in)
⚠️ Some TypeScript decorators may behave differently
Use Bun test for:
✅ Pure TypeScript/JavaScript logic tests
✅ API/server route tests
✅ Node.js utility library tests
✅ Integration tests without DOM
Use Vitest for:
✅ React component tests
✅ Browser environment simulation
✅ Projects using testing-library
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: Legacy but Still Dominant
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 (not Bun runtime)
→ React/Vue/Svelte components to test
→ Using Vite as build tool
→ Need Jest compatibility
→ Best balance of speed + ecosystem
Use Bun test if:
→ Already using Bun as runtime
→ Testing pure TypeScript logic (no DOM)
→ Maximum speed required
→ Server routes and API tests
Keep Jest if:
→ Large existing Jest codebase
→ Heavy reliance on Jest plugins
→ Not ready to migrate
→ Need maximum plugin compatibility
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",
}
`);
});
Bun test supports toMatchSnapshot() but does not support inline snapshots (toMatchInlineSnapshot()). This is a real limitation for teams that prefer inline snapshots for their locality — keeping the expected output adjacent to the assertion.
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. Bun test's snapshot implementation doesn't support custom serializers, which means dates and other complex types must be explicitly transformed 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. Jest API compatible, native TypeScript, excellent Vite and monorepo integration.
- Bun test: Choose when running Bun as runtime and testing pure TypeScript logic without DOM requirements. Fastest option for server-side logic tests.
- Jest: Maintain if you have a large, working Jest codebase with complex configuration. The single highest-impact improvement without full migration: replace Babel transform with SWC (
@swc/jest), which brings Jest 2-3x faster without any API changes.
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?
As of 2026, Bun test is stable for pure TypeScript/JavaScript testing without DOM requirements. It's used in production CI by teams testing backend services, API routes, and utility libraries. For React component testing, the happy-dom integration is functional but has occasional behavioral differences from jsdom that require workarounds. Teams with mixed test suites (component tests and backend tests) often run Vitest for component tests and Bun test for backend tests in the same CI pipeline, trading some configuration complexity for the speed benefit on the backend test subset.
Compare Vitest, Jest, and Bun download trends on PkgPulse. Related: Best JavaScript Testing Frameworks 2026 and Best JavaScript Runtimes 2026.