DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Vitest vs Jest in 2026: Migration Guide for TypeScript Projects

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-jest or ts-jest for 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
Enter fullscreen mode Exit fullscreen mode

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') },
  },
});
Enter fullscreen mode Exit fullscreen mode

Step 3: package.json

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui"
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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

  1. @testing-library/jest-dom still works — import in setup file:
// src/test/setup.ts
import '@testing-library/jest-dom';
Enter fullscreen mode Exit fullscreen mode
  1. moduleNameMapperresolve.alias in Vitest config
  2. __mocks__ supported but prefer vi.mock() inline
  3. 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:

Top comments (0)