How to Migrate from Jest to Vitest: Step-by-Step Guide
Vitest runs 28× faster than Jest in watch mode. This guide covers the full migration: install, configure globals, handle config files, and fix six gotchas.
By Ethan · Updated May 20, 2026
1,974 words · 10 min read
If your Jest suite is passing and your CI is fast enough, don’t migrate. If you’re spending real minutes on cold starts, fighting TypeScript transform config, or watching watch mode crawl behind your edits — this guide gets you to Vitest in a day.
Vitest 4.0 (released October 22, 2025) is stable, the API is 90% compatible with Jest, and the numbers are real: 5.6× faster cold start, 28× faster watch mode in a 50,000-test production monorepo. An automated codemod handles most of the rename work. The manual part is config files and a handful of behavioral differences that the codemod can’t catch.
Who this is for
Developers with an existing Jest suite on a TypeScript project — Vite-based or not. If you’re on React Native, stop here: Vitest doesn’t support the RN runtime and that won’t change soon. If you’re on Angular with a large Jest plugin ecosystem, read the Verdict section first.
If you haven’t committed to switching yet, read our Vitest vs Jest comparison first — it covers the tradeoffs without assuming you’ll migrate.
Why migrate from Jest to Vitest now
Three reasons to do it today rather than later.
Performance is not marketing copy. A production monorepo with 50,000 tests dropped cold start from 214 s to 38 s and watch re-run from 8.4 s to 0.3 s after migration. Those numbers come from a published case study of 3 engineers who migrated 82 test files over 2 weeks — and only disabled 1 test in the process. CI time fell from 14 minutes to under 5.
The ecosystem has shifted. State of JS 2024 ranks Vitest first in both retention and interest among testing libraries, with Jest ranked below it in both. Vitest is the recommended test runner in the official docs for Vue, Nuxt, SvelteKit, and Astro. Weekly npm downloads grew from 4.8M at v2.0 to 7.7M by v3.0.
Zero TypeScript setup. Jest still needs ts-jest or babel-jest with a Babel preset to understand TypeScript. Vitest uses esbuild natively — no transform config, no preset, no Babel. Vitest reads your tsconfig.json without any additional transform config.
Prerequisites
- Node 18+ (Vitest 4 requirement — check with
node -v) - An existing Jest test suite (CJS, ESM, or mixed — all work)
- A Vite-based project gets the most benefit; non-Vite projects still benefit but need a standalone
vitest.config.ts
Step 1: Install and configure
Remove Jest packages, install Vitest:
# Remove Jest
npm uninstall jest @types/jest babel-jest jest-environment-jsdom ts-jest
# Install Vitest + companions
npm install -D vitest @vitest/coverage-v8 jsdom
# For React projects add:
npm install -D @testing-library/react @testing-library/jest-dom
Create vitest.config.ts at the project root:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true, // exposes Vitest globals without imports (vi, describe, expect, etc.)
environment: 'jsdom', // or 'happy-dom' for 2-4x speedup (test compatibility first)
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8', // faster; switch to 'istanbul' if coverage numbers seem off
reporter: ['text', 'html', 'lcov'],
},
},
})
Then create vitest.setup.ts:
// vitest.setup.ts
// For @testing-library/jest-dom v6+, use the Vitest-specific entry:
import '@testing-library/jest-dom/vitest'
Failure mode: if you get Cannot find module '@vitejs/plugin-react', install it separately — it’s not bundled with Vitest. Non-React projects can drop the plugins array entirely. Non-Vite projects that have no vite.config.ts create vitest.config.ts as shown; Vite projects can merge the test block directly into their existing vite.config.ts.
Step 2: Update test scripts
Replace your Jest scripts in package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
vitest run is one-shot — equivalent to jest with no flags. vitest (no run) is watch mode, with HMR-aware change detection that only re-runs affected test files. The --ui flag opens a browser-based interactive runner; install @vitest/ui separately if you want it.
Step 3: Handle Jest globals
With globals: true in config, Vitest’s own globals — describe, it, test, expect, vi, beforeAll, afterAll, beforeEach, afterEach — are available without imports. The jest namespace is not available, even with globals: true. Vitest’s own migration guide is explicit: “Vitest doesn’t have an equivalent to jest namespace, so you will need to import types directly from vitest.” Any jest.* call left in your codebase will throw ReferenceError: jest is not defined. The codemod below is required to convert them.
Run the automated codemod first:
npx codemod jest/vitest
This converts jest.fn() → vi.fn(), jest.spyOn() → vi.spyOn(), and adds import { vi } from 'vitest' where needed. It covers roughly 80% of cases. Complex mock factories and type imports need manual review.
The full rename cheatsheet:
| Jest | Vitest | Notes |
|---|---|---|
jest.fn() | vi.fn() | Same signature |
jest.spyOn(obj, 'method') | vi.spyOn(obj, 'method') | Same signature |
jest.mock('./path') | vi.mock('./path') | Factory must return explicit { default, ... } object |
jest.requireActual('mod') | await vi.importActual('mod') | Must await — returns a Promise |
jest.useFakeTimers() | vi.useFakeTimers() | Same |
jest.useRealTimers() | vi.useRealTimers() | Same |
jest.clearAllMocks() | vi.clearAllMocks() | Same |
jest.resetAllMocks() | vi.resetAllMocks() | Behavior differs — see Gotchas |
jest.restoreAllMocks() | vi.restoreAllMocks() | Same |
jest.setTimeout(5000) | vi.setConfig({ testTimeout: 5000 }) | API changed |
JEST_WORKER_ID | VITEST_POOL_ID or VITEST_WORKER_ID | Both env vars available |
Type import change:
// Jest
let fn: jest.Mock<(name: string) => number>
// Vitest
import type { Mock } from 'vitest'
let fn: Mock<(name: string) => number>
Step 4: Handle config files
Babel: if you used Babel only for TypeScript or JSX in tests, remove it:
npm uninstall @babel/core @babel/preset-env @babel/preset-typescript babel-jest
Vitest handles TypeScript and JSX via esbuild. If you use Babel for production transforms outside of tests, keep it in your app build config and remove it from your test runner.
CSS and assets: Vitest handles CSS imports natively in most configurations. If you see errors on CSS module imports, add css: true to the test block.
moduleNameMapper migration: Jest’s path alias config maps to resolve.alias in Vitest:
// Jest (jest.config.js)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
}
// Vitest (vitest.config.ts)
import path from 'path'
// Inside defineConfig:
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
// CSS is handled natively — no styleMock needed in most cases
__mocks__ directory: Jest auto-applies files in __mocks__/. Vitest does not. Move auto-mock logic that you relied on into setupFiles instead, or call vi.mock() explicitly in each test file that needs it.
Step 5: Run and fix remaining failures
Run the suite and see what’s left:
npm test
The failure modes that survive the codemod fall into four categories.
@testing-library/jest-dom import path. This breaks all DOM assertions silently — toBeDisabled(), toBeInTheDocument(), everything. Fix it in vitest.setup.ts:
// Wrong (stops working after migration)
import '@testing-library/jest-dom'
// Correct for Vitest (v6+)
import '@testing-library/jest-dom/vitest'
Snapshot regeneration. Vitest’s snapshot format is compatible with Jest’s but the internal headers differ. Run once after migration:
vitest run --update
Vitest 4.0 also prints shadow DOM contents in snapshots by default. If that breaks existing snapshots, add printShadowRoot: false to your snapshot options.
Coverage provider mismatch. V8 (the default) is faster and adds near-zero runtime overhead. Istanbul adds 15–30% overhead but matches what you had with Jest. If you have coverage gates in CI, compare both outputs before committing to one — ignoreEmptyLines is on by default in Vitest 3+, which shifts the percentage slightly.
Complex mock factories. The codemod doesn’t fully handle cases where the factory returns implicit defaults. See Gotcha #2.
Real example — React Button test before and after:
// Button.test.tsx — BEFORE (Jest)
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import Button from './Button'
describe('Button', () => {
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('renders disabled state', () => {
render(<Button disabled>Click me</Button>)
expect(screen.getByText('Click me')).toBeDisabled()
})
})
// Button.test.tsx — AFTER (Vitest, globals: true)
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import Button from './Button'
describe('Button', () => {
it('calls onClick when clicked', () => {
const handleClick = vi.fn() // only change: jest.fn() → vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('renders disabled state', () => {
render(<Button disabled>Click me</Button>)
expect(screen.getByText('Click me')).toBeDisabled()
})
})
With globals: true, describe, it, and expect need no import. The only change in this file is jest.fn() → vi.fn(), which the codemod handles.
Gotchas
1. mockReset() behavior changed. Jest’s mockReset() replaces the implementation with () => undefined. Vitest’s mockReset() restores the original implementation you passed to vi.fn(impl). Any test that creates vi.fn(someImpl) and then calls mockReset() expecting undefined back will get the original function instead. The codemod doesn’t catch this — audit call sites manually.
2. Module mock factory must be an explicit object. Jest allowed returning a primitive from a mock factory. Vitest requires an explicit object with named exports:
// Jest — implicit default worked
jest.mock('./api', () => 'mocked-value')
// Vitest — must be explicit
vi.mock('./api', () => ({
default: 'mocked-value',
namedExport: vi.fn(),
}))
// Accessing the real module inside a factory:
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils')>()
return {
...actual,
formatDate: vi.fn(() => '2026-01-01'),
}
})
3. vi.importActual() is async. This replaces jest.requireActual(). Missing the await gives you a Promise object instead of the module — a runtime failure that doesn’t produce a useful error message.
4. jsdom vs happy-dom tradeoff. Start with jsdom — it’s what Jest used, your tests pass with zero changes. After the suite is green, switch to happy-dom to get 2–4× DOM test speedup. Known issues: MutationObserver edge cases, a few missing Web APIs. Run --reporter=verbose and compare before enabling in CI.
5. Test name separator changed. Jest uses a space: "describe test". Vitest uses >: "describe > test". Any CI filter that passes --testNamePattern with the old format will silently match nothing. Update grep patterns before deleting your Jest config.
6. Hook teardown return values. Vitest interprets a truthy return from beforeEach as a teardown callback and calls it after each test:
// Breaks in Vitest — implicit return from arrow
beforeEach(() => setActivePinia(createTestingPinia()))
// Safe — block body returns undefined
beforeEach(() => { setActivePinia(createTestingPinia()) })
Pinia’s setActivePinia returns a truthy object, so the arrow form registers it as a teardown. The symptom is intermittent test order failures that are hard to trace.
Verdict
Migrate if: you’re on a Vite project, TypeScript-first, paying real CI costs, or watching watch mode lag. The compatibility is genuine, the codemod covers the bulk of the work, and you’ll feel the performance difference on the first run.
Wait if: you’re on React Native (unsupported), you have a large CJS monorepo where Jest is stable and fast enough, or you rely on Jest plugins with no Vitest equivalents.
The migration effort for a 200–500 test suite is 1–3 days. For 82 files like the case study team, 2 weeks end-to-end — but they disabled only 1 test, which is a strong statement about compatibility.
Jest is not going away. Meta still maintains it, and it remains the right choice for React Native and legacy CJS codebases. But for any new Vite or TypeScript-first project in 2026, choosing Jest needs a specific justification.
If you’re also modernizing your bundler, our Webpack to Vite migration guide covers the build-tool side of the same TypeScript-first shift.