Jest has dominated JavaScript testing for a decade. Vitest is the modern replacement. If you use Vite, ESM, or TypeScript with modern imports, migration takes 30 minutes and performance improvements are immediate.
Why Migrate?
Jest's problems:
- Requires
babel-jestorts-jestfor TypeScript (slow, config overhead) - CommonJS-first — ESM support is experimental and painful
- Cold starts brutal on large codebases
- Config duplicates
vite.config.ts
Vitest advantages:
- Native ESM + TypeScript, no transpilation
- Vite config reuse
- 2-10x faster cold start
- Jest-compatible API — mostly find-and-replace migration
- HMR: only affected tests re-run on file change
- Built-in V8 coverage
Migration in 4 Steps
Step 1: Swap packages
npm uninstall jest ts-jest @types/jest babel-jest jest-environment-jsdom
npm install -D vitest @vitest/coverage-v8 jsdom @testing-library/jest-dom
Step 2: vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules/', 'src/test/'],
},
},
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
});
Step 3: package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
Step 4: jest → vi
// Before
import { jest } from '@jest/globals';
const mockFn = jest.fn();
jest.spyOn(module, 'method');
jest.mock('./module');
// After
import { vi } from 'vitest';
const mockFn = vi.fn();
vi.spyOn(module, 'method');
vi.mock('./module');
With globals: true, describe/it/expect/beforeEach/afterEach work without imports.
Real Examples
API Route
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET } from './route';
import { db } from '@/lib/db';
vi.mock('@/lib/db', () => ({
db: { user: { findMany: vi.fn() } },
}));
describe('GET /api/users', () => {
beforeEach(() => vi.clearAllMocks());
it('returns users', async () => {
vi.mocked(db.user.findMany).mockResolvedValue([{ id: '1', email: 'test@example.com' }]);
const response = await GET();
expect(response.status).toBe(200);
});
});
Fake Timers
it('retries on failure', async () => {
vi.useFakeTimers();
const fn = vi.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValueOnce({ data: 'ok' });
const result = retryFetch(fn, { maxRetries: 3, delay: 1000 });
await vi.advanceTimersByTimeAsync(1000);
expect(await result).toEqual({ data: 'ok' });
expect(fn).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
Performance
On 200 test files / ~1500 tests:
| Runner | Cold Start | Changed-file rerun |
|---|---|---|
| Jest + ts-jest | 18-25s | 12-18s |
| Vitest | 4-7s | <1s (HMR) |
Common Gotchas
-
@testing-library/jest-domstill works — import in setup file:
// src/test/setup.ts
import '@testing-library/jest-dom';
-
moduleNameMapper→resolve.aliasin Vitest config -
__mocks__supported but prefervi.mock()inline - Snapshots work identically (
toMatchSnapshot())
Start with utility tests, verify they pass, then migrate components. Lower risk than it looks.
Ship Your SaaS Faster
Stop reinventing the wheel. whoffagents.com has everything you need:
- 🚀 AI SaaS Starter Kit — Full-stack Next.js + Stripe + Auth in one repo ($99)
- ⚡ Ship Fast Skill Pack — 50+ Claude Code skills for 10x faster development ($49)
- 🔒 MCP Security Scanner — Audit your MCP servers before they hit production ($29)
Top comments (0)