Skip to main content

Bun Test vs Vitest vs Jest: JavaScript Testing in 2026

·PkgPulse Team

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

FeatureJestVitestBun test
Weekly downloads55M6MN/A (built-in)
Startup time~1.2s~0.9s~0.08s
Watch mode speed~800ms~40ms (HMR)~50ms
TypeScript supportVia ts-jest/BabelNative (Vite)Native (Bun)
ESM supportAwkwardNativeNative
Jest API compatNativeYesMostly
Browser modeNoYes (Playwright)No
CoverageYesYes (v8/istanbul)Yes (V8)
Snapshot testingYesYesYes
Requires ViteNoRecommendedNo
Requires BunNoNoYes

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.