Bun Test vs Vitest vs Jest: JavaScript Testing in 2026
Bun's test runner completes in 0.08 seconds what takes Jest 1.2 seconds and Vitest 0.9 seconds. Vitest runs 10-20x faster than Jest in watch mode with HMR. Jest has 55M+ weekly downloads and the most mature ecosystem. The choice comes down to what you optimize: raw speed, framework integration, or ecosystem breadth.
TL;DR
Vitest for Vite-based projects and most new TypeScript applications — 10-20x faster than Jest, Jest-compatible API, excellent HMR in watch mode. Bun test when you're already using Bun as your runtime and want the absolute fastest test execution. Jest for existing projects with complex configuration, Jest-specific plugins (Enzyme, jest-axe, etc.), or non-Vite setups that don't need migration overhead. For new projects starting today, Vitest is the pragmatic default.
Key Takeaways
- Jest: 55M weekly downloads, mature ecosystem, 0.x startup overhead
- Vitest: 6M weekly downloads, Vite integration, HMR watch mode, 10-20x faster than Jest
- Bun test: Built-in (no install), 0.08s for simple suites, Jest-compatible API
- All three: describe/it/test, expect, mocking (vi.mock / jest.mock / mock()), snapshots
- Vitest: Native ESM support, native TypeScript (via Vite), browser mode (Playwright)
- Jest: jsdom by default, extensive plugin ecosystem, monorepo-tested
- Bun test: Built-in coverage, requires Bun runtime, snapshot testing
Speed Comparison
# Test suite: 50 tests across 10 files, TypeScript, mocking
# Cold run (no cache):
Jest: ~1.2s
Vitest: ~0.9s
Bun: ~0.08s
# Watch mode (single file change):
Jest: ~800ms (re-runs all)
Vitest: ~40ms (HMR — only re-runs changed)
Bun: ~50ms (watch mode)
Speed matters most in watch mode during development. Vitest's HMR re-runs only affected tests in ~40ms — near-instant feedback compared to Jest's 800ms.
Jest
Package: jest, @jest/core, ts-jest
Weekly downloads: 55M
GitHub stars: 44K
Creator: Facebook / Meta
Jest is the established standard: it ships with Create React App, NestJS, and most major frameworks by default. Its ecosystem is the largest of the three.
Installation
npm install -D jest ts-jest @types/jest
# Or with Babel for TypeScript:
npm install -D jest babel-jest @babel/preset-typescript @babel/core
Configuration
// jest.config.json
{
"preset": "ts-jest",
"testEnvironment": "node",
"roots": ["<rootDir>/src"],
"testMatch": ["**/*.spec.ts", "**/*.test.ts"],
"moduleNameMapper": {
"@/(.*)": "<rootDir>/src/$1"
},
"setupFilesAfterEach": ["<rootDir>/jest.setup.ts"],
"collectCoverageFrom": ["src/**/*.ts", "!src/**/*.d.ts"]
}
Basic Test
// user.service.test.ts
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
// Jest mock
jest.mock('./user.repository');
const MockedUserRepository = UserRepository as jest.MockedClass<typeof UserRepository>;
describe('UserService', () => {
let service: UserService;
let mockRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
MockedUserRepository.mockClear();
mockRepo = new MockedUserRepository() as jest.Mocked<UserRepository>;
service = new UserService(mockRepo);
});
it('returns user by id', async () => {
mockRepo.findById.mockResolvedValue({
id: '123',
name: 'Alice',
email: 'alice@example.com',
});
const user = await service.getUser('123');
expect(user.name).toBe('Alice');
expect(mockRepo.findById).toHaveBeenCalledWith('123');
expect(mockRepo.findById).toHaveBeenCalledTimes(1);
});
it('throws when user not found', async () => {
mockRepo.findById.mockResolvedValue(null);
await expect(service.getUser('999')).rejects.toThrow('User 999 not found');
});
});
Jest for React (with Testing Library)
npm install -D @testing-library/react @testing-library/jest-dom jest-environment-jsdom
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders label', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick', () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
Jest Snapshot Testing
it('renders button correctly', () => {
const { asFragment } = render(<Button variant="primary">Submit</Button>);
expect(asFragment()).toMatchSnapshot();
});
Jest Strengths
- Largest ecosystem: jest-axe, jest-fetch-mock, jest-canvas-mock, Enzyme
- Snapshot testing with automatic diff display
- Most documentation and Stack Overflow answers
- Works with any build tool (not Vite-specific)
- Used as the default in NestJS, CRA, Angular (historically)
Jest Limitations
- Slow startup (TypeScript compiler, Babel transforms)
- ESM support remains awkward (requires
--experimental-vm-modules) - No HMR in watch mode (re-runs all tests)
- Significant memory usage for large test suites
Vitest
Package: vitest
Weekly downloads: 6M
GitHub stars: 13K
Creator: Anthony Fu / Vite team
Vitest is built on Vite's infrastructure. It uses the same module graph as Vite, enabling HMR for tests — changed tests re-run in milliseconds, not seconds.
Installation
npm install -D vitest
# For React tests:
npm install -D @testing-library/react @testing-library/jest-dom @vitejs/plugin-react jsdom
Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // or 'node', 'happy-dom'
globals: true, // No need to import describe/it/expect
setupFiles: ['./src/test/setup.ts'],
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
alias: {
'@': new URL('./src', import.meta.url).pathname,
},
},
});
Basic Test (Identical to Jest)
// user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
// Vitest mock — same API as Jest
vi.mock('./user.repository');
describe('UserService', () => {
let service: UserService;
let mockRepo: ReturnType<typeof vi.mocked<typeof UserRepository>>;
beforeEach(() => {
vi.clearAllMocks();
// Same pattern as Jest
});
it('returns user by id', async () => {
vi.mocked(mockRepo.findById).mockResolvedValue({
id: '123',
name: 'Alice',
email: 'alice@example.com',
});
const user = await service.getUser('123');
expect(user.name).toBe('Alice');
});
});
The test code is almost identical to Jest — migration is usually a matter of renaming jest. to vi..
Vitest HMR Watch Mode
vitest --watch
# Single file changes: re-runs only affected tests in ~40ms
# Full re-runs only when configuration changes
This is the key Vitest advantage: if you change user.service.ts, Vitest knows exactly which tests import it and re-runs only those — without restarting the entire test runner.
Vitest Native ESM
// No --experimental-vm-modules flag needed
// ESM imports just work:
import { something } from './my-esm-module.mjs';
Vitest Browser Mode
// vitest.config.ts
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright', // or 'webdriverio'
name: 'chromium',
},
},
});
Run tests in a real browser (not jsdom) — same API, real rendering engine.
Vitest Strengths
- 10-20x faster than Jest in watch mode (HMR)
- Native ESM and TypeScript (no transpilation config)
- Jest-compatible API (minimal migration effort)
- Vite-based: shares config with your Vite app
- Browser mode for real browser testing
- Excellent IntelliSense and VS Code integration
Vitest Limitations
- Vite-centric: less natural for non-Vite projects (webpack, Rollup, etc.)
- Smaller ecosystem than Jest (some Jest-specific plugins don't work)
- Browser mode is newer and less documented than Jest's jsdom
Bun Test
Part of: Bun runtime (bun.sh) API: Jest-compatible No npm install required
Bun's built-in test runner is the fastest option — written in Zig, it runs 15x faster than Jest on equivalent suites.
No Installation
# bun test is built-in — no package.json changes:
bun test
# Watch mode:
bun test --watch
# With coverage:
bun test --coverage
Writing Tests (Identical API)
// user.service.test.ts
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { UserService } from './user.service';
// Bun's mock function (similar to jest.fn())
const mockFindById = mock();
describe('UserService', () => {
beforeEach(() => {
mockFindById.mockClear();
});
it('returns user by id', async () => {
mockFindById.mockResolvedValue({
id: '123',
name: 'Alice',
email: 'alice@example.com',
});
// ... test code
});
});
Bun Test Snapshot Testing
import { test, expect } from 'bun:test';
test('renders correctly', () => {
const output = render(<Button>Click</Button>);
expect(output).toMatchSnapshot();
// Creates __snapshots__/button.test.tsx.snap
});
Coverage
bun test --coverage
# Output:
# | File | Stmts | Branch | Funcs | Lines |
# | user.service.ts | 95.2% | 88.5% | 100% | 95.2% |
Built-in V8 coverage — no @vitest/coverage-v8 or istanbul needed.
Bun Test Limitations
- Requires Bun runtime (can't run on Node.js)
- Not all Jest plugins work (jest-axe, Enzyme, etc.)
- Less mature ecosystem than Jest/Vitest
- Module mocking is less ergonomic than Jest/Vitest
Migration: Jest → Vitest
Most Jest tests migrate with minimal changes:
npm uninstall jest ts-jest @types/jest babel-jest
npm install -D vitest @vitest/coverage-v8
// Before (Jest):
import { jest } from '@jest/globals';
jest.mock('./module');
const mock = jest.fn();
// After (Vitest):
import { vi } from 'vitest';
vi.mock('./module');
const mock = vi.fn();
In vitest.config.ts, set globals: true to avoid importing describe, it, expect in every test file — maintaining Jest compatibility.
Feature Comparison
| Feature | Jest | Vitest | Bun test |
|---|---|---|---|
| Weekly downloads | 55M | 6M | N/A (built-in) |
| Startup time | ~1.2s | ~0.9s | ~0.08s |
| Watch mode speed | ~800ms | ~40ms (HMR) | ~50ms |
| TypeScript support | Via ts-jest/Babel | Native (Vite) | Native (Bun) |
| ESM support | Awkward | Native | Native |
| Jest API compat | Native | Yes | Mostly |
| Browser mode | No | Yes (Playwright) | No |
| Coverage | Yes | Yes (v8/istanbul) | Yes (V8) |
| Snapshot testing | Yes | Yes | Yes |
| Requires Vite | No | Recommended | No |
| Requires Bun | No | No | Yes |
Choosing Your Test Runner in 2026
Use Vitest if:
- Your project uses Vite (Next.js via Vite, Nuxt, Astro, SvelteKit, most modern setups)
- You want a fast Jest drop-in with minimal migration
- TypeScript and ESM without transpilation config is important
- You want browser mode testing
Use Jest if:
- Large existing Jest codebase with complex mocking patterns
- You use Jest-specific plugins (Enzyme, jest-axe, jest-fetch-mock)
- NestJS, CRA, or other frameworks that configure Jest by default
- Non-Vite build setup where Vitest integration is awkward
Use Bun test if:
- Your entire project runs on Bun (not just Node.js)
- Raw speed is the priority and Jest API compatibility is close enough
- You want a zero-dependency test runner for simple utilities
Compare testing library trends on PkgPulse.
See the live comparison
View bun test vs. vitest vs jest on PkgPulse →