Skip to main content

How to Migrate from Jest to Vitest in 30 Minutes

·PkgPulse Team

TL;DR

Most Jest suites migrate to Vitest in under an hour. The APIs are nearly identical — describe, it, expect, vi (instead of jest) — and Vitest's configuration is simpler than Jest's. The main differences: jest global becomes vi, module mocking uses vi.mock(), and you configure via vitest.config.ts instead of jest.config.js. Speed improvement after migration: typically 3-10x faster test runs.

Key Takeaways

  • 95%+ of Jest API is identical in Vitest — most tests need zero changes
  • jestvi — the only global name change
  • Config is simpler — no babel, no transform config for TypeScript
  • 3-10x faster — native ESM + esbuild vs Jest's Babel transforms
  • Vite-powered — uses your existing Vite config, same aliases and plugins

Step 1: Install Vitest

# Remove Jest dependencies
npm uninstall jest @types/jest ts-jest babel-jest @babel/preset-env
pnpm remove jest @types/jest ts-jest babel-jest

# Install Vitest
npm install -D vitest @vitest/ui
pnpm add -D vitest @vitest/ui

# For React component testing
npm install -D @testing-library/react @testing-library/jest-dom jsdom
# (likely already installed if using React Testing Library)

Step 2: Update package.json Scripts

// Before (Jest):
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --forceExit"
  }
}

// After (Vitest):
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui",
    "test:ci": "vitest run --coverage"
  }
}

Step 3: Create vitest.config.ts

// vitest.config.ts — replaces jest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [
    react(),
    tsconfigPaths(),  // Resolves TypeScript path aliases
  ],
  test: {
    // Test environment
    environment: 'jsdom',       // or 'node' for Node.js-only tests
    globals: true,              // Makes describe/it/expect global (no import needed)
    setupFiles: ['./src/test/setup.ts'],

    // Coverage
    coverage: {
      provider: 'v8',           // Faster than c8
      reporter: ['text', 'lcov', 'json'],
      exclude: ['**/*.d.ts', '**/*.config.*', '**/test/**'],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 70,
      },
    },

    // File patterns
    include: ['**/*.{test,spec}.{ts,tsx,js,jsx}'],
    exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'],

    // Performance
    pool: 'threads',            // Default, fastest for CPU-intensive tests
    poolOptions: {
      threads: {
        singleThread: false,    // Run tests in parallel
      },
    },
  },
});
// src/test/setup.ts — replaces src/setupTests.ts
import '@testing-library/jest-dom';
// Add any other global setup here

Step 4: Update Imports (Usually Not Needed)

// If you DON'T use `globals: true`, add this to every test file:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// With `globals: true` in config, your existing test files work as-is
// No import needed — describe/it/expect are globally available
// (same behavior as Jest's default)

Step 5: Replace jest with vi

// This is the main code change: jest.* → vi.*

// Before (Jest):
jest.fn()
jest.spyOn(obj, 'method')
jest.mock('./module')
jest.clearAllMocks()
jest.resetAllMocks()
jest.useFakeTimers()
jest.useRealTimers()
jest.advanceTimersByTime(1000)
jest.runAllTimers()
jest.setTimeout(10000)

// After (Vitest):
vi.fn()
vi.spyOn(obj, 'method')
vi.mock('./module')
vi.clearAllMocks()
vi.resetAllMocks()
vi.useFakeTimers()
vi.useRealTimers()
vi.advanceTimersByTime(1000)
vi.runAllTimers()
vi.setConfig({ testTimeout: 10000 })

// The type for mocked functions also changes:
// Before: jest.Mock → jest.MockedFunction<typeof fn>
// After:  vi.Mock → vi.MockedFunction<typeof fn>
// Or use: ReturnType<typeof vi.fn>

Common Migration Gotchas

Gotcha 1: Module Mocking Syntax

// Jest:
jest.mock('./utils', () => ({
  formatDate: jest.fn().mockReturnValue('2026-01-01'),
}));

// Vitest — same syntax works:
vi.mock('./utils', () => ({
  formatDate: vi.fn().mockReturnValue('2026-01-01'),
}));

// Vitest — auto-mock (new feature, cleaner):
vi.mock('./utils');
// All exports automatically become vi.fn()

// Vitest — mock factory must return default correctly:
vi.mock('./api-client', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./api-client')>();
  return {
    ...actual,
    fetchUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
  };
});

Gotcha 2: jest.requireActualvi.importActual

// Jest:
const actual = jest.requireActual('./module');

// Vitest:
const actual = await vi.importActual('./module');
// Note: vi.importActual is async — use await

// In a mock factory:
vi.mock('./date-utils', async () => {
  const actual = await vi.importActual<typeof import('./date-utils')>('./date-utils');
  return {
    ...actual,
    getNow: vi.fn().mockReturnValue(new Date('2026-01-01')),
  };
});

Gotcha 3: Environment per Test File

// In Jest: configure per-file in the file itself
// @jest-environment jsdom

// In Vitest: same annotation syntax works
// @vitest-environment jsdom

// Or configure per directory via vitest.config.ts environmentMatchGlobs
test: {
  environmentMatchGlobs: [
    ['**/*.browser.test.ts', 'jsdom'],
    ['**/*.node.test.ts', 'node'],
  ],
}

Gotcha 4: TypeScript rootDir for globals: true

// tsconfig.json — add Vitest types when using globals
{
  "compilerOptions": {
    "types": ["vitest/globals"]  // Adds describe/it/expect type declarations
  }
}

Automated Migration

# @vitest/migrate-from-jest — official migration tool (beta)
npx @vitest/migrate-from-jest

# What it does:
# - Updates jest.config.js → vitest.config.ts
# - Replaces jest.fn() → vi.fn() across all test files
# - Updates import statements
# - Reports what it couldn't auto-migrate

# Always review the diff before committing:
git diff --stat

Performance Comparison

# Real benchmark: medium React app (200 test files, ~1500 tests)

# Jest (Babel transform):
# Test run: 45 seconds
# Watch mode first run: 12 seconds
# Hot reload: 4 seconds per file

# Vitest (esbuild transform):
# Test run: 8 seconds  ← 5.6x faster
# Watch mode first run: 3 seconds
# Hot reload: 0.8 seconds per file

# Why Vitest is faster:
# 1. esbuild transforms TypeScript 10x faster than Babel
# 2. Native ESM — no CommonJS runtime overhead
# 3. Worker pool reuses transformed modules
# 4. Parallel file execution is better utilized

Compare Vitest and Jest package health on PkgPulse.

Comments

Stay Updated

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