Skip to main content

How to Migrate from Jest to Vitest in 2026

·PkgPulse Team
0

If your test suite is slow, migrating from Jest to Vitest is one of the highest-leverage improvements you can make to your development workflow. Vitest uses esbuild for TypeScript transforms (instead of Babel) and runs tests with native ESM support, producing 3-10x faster test execution. The API is intentionally Jest-compatible, which means most migrations are straightforward. This guide covers the full process including the common gotchas that aren't well-documented elsewhere.

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.

Quick Comparison

VitestJest
Weekly Downloads~5M~20M
Transformesbuild (fast)Babel (slower)
ESM Support✅ Native⚠️ Experimental
TypeScript✅ No config✅ Needs ts-jest
Speed3-10x fasterBaseline
Watch Mode✅ Excellent✅ Good
Coveragev8 or istanbulistanbul
Vite Integration✅ First-class
LicenseMITMIT

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

Why Migrate?

The performance argument for Vitest is compelling. Jest uses Babel to transform TypeScript and JSX before running tests — Babel is battle-tested but slow. Vitest uses esbuild, which performs the same transformations roughly 10-100x faster. For a test suite with 200 files, this difference in transform time alone accounts for most of the speed improvement.

The ESM story matters for modern JavaScript projects. Jest's ESM support is experimental and has known limitations — some packages that ship only ESM modules cause configuration headaches in Jest. Vitest runs tests in a native ESM environment where ESM packages just work. If you've ever spent an afternoon configuring Jest to handle a package that ships ESM-only, you understand why this matters.

Vite integration is the third argument. If your project uses Vite for development (which includes most React, Vue, and Svelte projects in 2026), Vitest can reuse your Vite configuration directly — the same plugins, the same path aliases, the same environment configuration. You define your TypeScript path aliases once in vite.config.ts and they work in both the dev server and tests without duplication.


Step 1: Install Vitest

The installation step is straightforward — remove Jest's dependencies and install Vitest. The key insight is that Vitest doesn't need Babel for TypeScript transformation, which removes several packages from your devDependencies.

# 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)

The @vitest/ui package provides an excellent browser-based test UI — a nice addition if you're debugging failing tests and want to see component renders and test output side by side.


Step 2: Update package.json Scripts

Script names change from Jest conventions to Vitest conventions. The main difference: vitest run for CI (single pass, no watch), and vitest (no run) for watch mode in development.

// 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"
  }
}

Note that Vitest's watch mode is the default when you run vitest without run. In CI environments, always use vitest run to prevent the process from hanging waiting for file changes.


Step 3: Create vitest.config.ts

This is the most important step and where most migrations require careful attention. Vitest's configuration replaces both jest.config.js and any Babel configuration you had for Jest. TypeScript support, path aliases, and test environments are all configured here.

// 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

The vite-tsconfig-paths plugin is worth highlighting. It reads your tsconfig.json paths configuration and applies them in Vitest. If you have path aliases like @/components → src/components, you only need to define them once in tsconfig.json — the plugin makes them work in tests automatically, no duplication in vitest.config.ts.


Step 4: Update Imports (Usually Not Needed)

With globals: true in your Vitest config, your existing test files work without importing describe, it, or expect. This matches Jest's default behavior where these are global. Most codebases can skip this step entirely.

// 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)

If you prefer explicit imports over globals (for better IDE analysis and avoiding implicit dependencies), Vitest's explicit import API is cleaner than Jest's. You get full TypeScript autocomplete on all test utilities when imported explicitly.


Step 5: Replace jest with vi

This is the main code change required in most test files. The jest global becomes vi — same API, different name. A global find-and-replace handles 95% of this step.

// 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>

The jest.setTimeoutvi.setConfig({ testTimeout }) change is easy to miss in a find-and-replace. It's worth searching specifically for jest.setTimeout after the global replacement.


Common Migration Gotchas

Gotcha 1: Module Mocking Syntax

Module mocking works the same way in Vitest as Jest, with one key addition: vi.importActual is async where jest.requireActual was synchronous.

// 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

The async change here catches many migrations. If you're using jest.requireActual inside a mock factory, you need to make the factory async.

// 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 provides an official migration tool that handles most of the file-level changes automatically.

# @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

Even with the automated tool, plan for a code review of the diff. The tool handles the mechanical changes well but can miss edge cases in complex mock configurations.


Performance Comparison

The performance gains are the main reason to migrate. On real codebases, 3-10x faster test runs meaningfully change how developers interact with the test suite — tests you'd skip running locally because they're slow become fast enough to run on every save.

# 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

The watch mode improvement matters as much as the CI run improvement. When watch mode first-start drops from 12 seconds to 3 seconds, developers actually keep it running. When it stays at 12 seconds, developers close it and run tests manually, which means running tests less frequently, which means catching bugs later.


Testing with the Vitest UI

One of Vitest's best features that Jest doesn't have is the browser-based UI. Running vitest --ui opens a web interface showing all your tests, their status, and detailed output. You can filter tests, re-run specific files, and see component snapshots if you're using Vitest's browser mode.

For debugging failing tests, the UI mode shows the full test output including stack traces with source-mapped TypeScript line numbers. Combined with Vitest's fast HMR, you can iterate on a failing test in the UI and see results update in under a second.


Coverage and Reporting

Vitest supports two coverage providers: v8 (built into Node.js) and istanbul. V8 coverage is faster since it uses the JavaScript engine's native coverage tracking. Istanbul coverage is more mature and has better support for edge cases in coverage calculation.

For most projects, start with v8 coverage and switch to istanbul if you encounter inaccurate coverage numbers for complex code paths. The configuration is a single line change in vitest.config.ts.

Coverage reporting output formats are compatible between Jest and Vitest — both support lcov (for Codecov/Coveralls), json, text, and html. Your CI coverage upload scripts likely won't need changes.


Snapshot Testing

Vitest's snapshot testing is compatible with Jest's snapshot format. Existing .snap files from Jest work in Vitest without modification. The expect(value).toMatchSnapshot() and expect(value).toMatchInlineSnapshot() APIs are identical.

One improvement in Vitest: inline snapshots are formatted more cleanly and snapshot updates happen in-process (no separate update pass needed for many cases). Run vitest --update to update snapshots, same as Jest's --updateSnapshot.


Browser Mode (Advanced)

For projects that need to test components in a real browser environment (rather than jsdom), Vitest has an experimental browser mode that runs tests in Chromium via Playwright. This is more than jsdom for testing Web Animations, Web Bluetooth, or other APIs that jsdom doesn't implement.

// vitest.config.ts for browser mode:
export default defineConfig({
  test: {
    browser: {
      enabled: true,
      name: 'chromium',
      provider: 'playwright',
    },
  },
});

Browser mode is significantly slower than jsdom (real browser startup overhead) and should only be used for tests that genuinely need browser APIs. For the vast majority of React/Vue component tests, jsdom provides the right behavior at much lower overhead.


Is Jest Still Valid?

Yes — Jest is still actively maintained (Meta uses it at massive scale) and is a fine choice for projects that are already on it. The migration has a clear payoff (3-10x speed), but if your test suite already runs in under a minute and you're not experiencing ESM-related headaches, the migration is a nice-to-have rather than a must-have.

New projects should default to Vitest. Existing Jest projects should migrate when they have time or when ESM compatibility issues start causing pain. The migration is low-risk enough that there's no reason to stay on Jest indefinitely, but no emergency to migrate today either.


Integration With CI/CD

Vitest integrates well with all major CI providers without special configuration. The vitest run command exits with a non-zero code on test failures, which CI systems use to fail the pipeline. Coverage reports are written to disk in the configured format and can be uploaded to Codecov, Coveralls, or any other coverage service using the same upload scripts you used with Jest.

GitHub Actions example:

- name: Run tests
  run: pnpm test:ci
  
- name: Upload coverage
  uses: codecov/codecov-action@v4
  with:
    files: ./coverage/lcov.info

The --reporter=verbose flag provides detailed output in CI logs. The --reporter=junit flag generates JUnit XML output for CI systems that parse test results (like GitLab CI and Azure DevOps).


Monorepo Considerations

In a monorepo with multiple packages, each package can have its own Vitest configuration, or you can use a root-level configuration with project references. Vitest's workspace mode (vitest.workspace.ts) lets you run all package tests from the root while respecting package-level configurations.

// vitest.workspace.ts (root-level):
export default [
  'packages/*/vitest.config.ts',  // Use each package's own config
  // OR inline config for each workspace:
  {
    test: {
      name: 'packages/utils',
      root: './packages/utils',
    }
  }
];

This integrates well with Turborepo and Nx — define your test script as vitest run in each package, and the monorepo orchestrator handles running them in parallel with dependency order respect.


Summary: The Case for Migrating

The case for migrating from Jest to Vitest in 2026 is simple: Vitest is faster, simpler to configure for modern TypeScript projects, and has a superset of Jest's API. The migration risk is low — the official migration tool handles the mechanical changes, the API is nearly identical, and your existing tests continue to work.

The main reason to stay on Jest is organizational inertia or significant investment in Jest-specific tooling. Both are valid reasons. But for teams doing any greenfield work, starting new packages with Vitest rather than Jest is a clear improvement with no meaningful downside.


Compare Vitest and Jest package health at PkgPulse →

Related: Vite vs Webpack 2026 · Best testing libraries for React 2026 · Browse testing packages

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.