Technology · Developer Tooling
Vitest in 2026: The Testing Setup That Replaced Jest for Our Team
Vitest runs faster, works natively with ESM and TypeScript, and uses the same API as Jest. If you're still on Jest in a Vite-based project, here's the case for switching and exactly how to do it.
Anurag Verma
6 min read
Sponsored
Jest built the culture of component testing in the JavaScript ecosystem. It’s been the default for years. But it was built for a CommonJS world, and modern JavaScript is ESM by default. The mismatches (needing babel to transform files, broken ESM imports, TypeScript source maps that don’t match, Jest configs that grow into multi-page files) have accumulated into real friction.
Vitest was built alongside Vite and shares its configuration. If you’re using Vite (which means Next.js with the Vitest adapter, Astro, SvelteKit, Remix, or a Vite-based framework), your tests run against the same pipeline that builds your app. No separate babel config. No separate TypeScript configuration. No separate module resolution rules.
The result is a test runner that feels like it was actually designed for 2026.
Why Vitest Over Jest
ESM without ceremony. Jest needs --experimental-vm-modules and specific transforms to handle ESM. Vitest handles ESM natively. If your app code is ESM, your tests are too.
TypeScript without extra setup. Jest requires ts-jest or babel to transform TypeScript. Vitest uses the same TypeScript transforms as Vite. Add it to your project and it works.
Shared configuration. Jest has jest.config.js alongside your vite.config.ts. Vitest configuration lives inside vite.config.ts. One fewer config file and no divergence between how tests and app code resolve modules.
Speed. Vitest runs tests in parallel across worker threads and only re-runs tests affected by your code changes. On large test suites, the warmup and re-run time is lower than Jest in most comparisons.
Same API. describe, it, expect, vi.fn(), vi.mock(), beforeEach, afterEach. If you know Jest, you know Vitest.
Setup
npm install -D vitest @vitest/ui
Add to vite.config.ts:
import { defineConfig } from 'vite';
import { defineConfig as defineTestConfig } from 'vitest/config';
export default defineConfig({
// ... your existing vite config
test: {
environment: 'jsdom', // 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime'
globals: true, // makes describe/it/expect globally available without imports
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
},
},
});
Add to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}
vitest starts in watch mode. vitest run runs once and exits (for CI).
Writing Tests
The API is Jest-compatible:
// src/utils/format.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { formatCurrency, formatDate } from './format';
describe('formatCurrency', () => {
it('formats USD amounts', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
it('handles zero', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00');
});
it('handles negative amounts', () => {
expect(formatCurrency(-100, 'USD')).toBe('-$100.00');
});
});
If you have globals: true in your config, you can skip the imports:
// No import needed when globals: true
describe('formatCurrency', () => {
it('formats USD amounts', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
});
Mocking
Vitest uses vi where Jest uses jest:
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { sendEmail } from '../services/email';
import { notifyUser } from './notifications';
vi.mock('../services/email');
describe('notifyUser', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('sends an email on signup', async () => {
vi.mocked(sendEmail).mockResolvedValue({ sent: true });
await notifyUser({ event: 'signup', userId: '123' });
expect(sendEmail).toHaveBeenCalledWith({
to: expect.any(String),
subject: 'Welcome!',
});
});
});
Module mocking works with vi.mock() at the top level, same as Jest’s jest.mock().
Spying on implementations:
import { vi, it, expect } from 'vitest';
it('calls the callback after delay', async () => {
vi.useFakeTimers();
const callback = vi.fn();
scheduleCallback(callback, 1000);
expect(callback).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1000);
expect(callback).toHaveBeenCalledOnce();
vi.useRealTimers();
});
Component Testing With React Testing Library
For React components:
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
Setup file:
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
Test:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi, it, expect, describe } from 'vitest';
import { SearchInput } from './SearchInput';
describe('SearchInput', () => {
it('calls onSearch when the user types', async () => {
const user = userEvent.setup();
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} />);
const input = screen.getByRole('textbox');
await user.type(input, 'hello');
expect(onSearch).toHaveBeenCalledWith('hello');
});
});
This is identical to how you’d write the test with Jest + RTL. The same test file works with either runner, which makes migration straightforward.
Migrating From Jest
For most projects, migration is mechanical:
- Install Vitest and remove Jest packages
- Replace
jest.config.jswith thetestblock invite.config.ts - Replace
jestwithviin test files - Update CI script from
jesttovitest run
# Remove Jest
npm uninstall jest jest-environment-jsdom @types/jest ts-jest babel-jest
# Install Vitest
npm install -D vitest @vitest/coverage-v8 jsdom
If you used @types/jest for global types, add this to tsconfig.json instead:
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Search and replace across your test files:
jest.fn()→vi.fn()jest.mock(→vi.mock(jest.spyOn(→vi.spyOn(jest.clearAllMocks()→vi.clearAllMocks()jest.useFakeTimers()→vi.useFakeTimers()jest.resetModules()→vi.resetModules()
If you have jest.config.js options like moduleNameMapper or transform, they translate to Vite’s resolve.alias and plugin configuration.
The Vitest UI
Vitest ships a browser-based UI:
npx vitest --ui
This opens a dashboard showing all tests, pass/fail state, duration, and coverage. Useful during active development to see the full test tree without scrolling through terminal output. Optional; it doesn’t affect the test runner itself.
In-Source Tests
A unique Vitest feature: tests embedded directly in your source files.
// src/utils/math.ts
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
// Only runs during testing, stripped from production builds
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it('clamps to min', () => {
expect(clamp(-5, 0, 10)).toBe(0);
});
it('clamps to max', () => {
expect(clamp(15, 0, 10)).toBe(10);
});
it('returns value within range', () => {
expect(clamp(5, 0, 10)).toBe(5);
});
}
Useful for utility functions where you want the test immediately adjacent to the code. Not suitable for components or integration tests; those stay in separate *.test.ts files.
When Jest Is Still Fine
If your project doesn’t use Vite, the case for migrating is weaker. Next.js 14+ with its SWC compiler handles Jest well without Babel, and the module resolution issues that plagued older setups are mostly solved. Moving a stable, well-configured Jest setup to Vitest purely for speed is probably not worth the migration risk.
If you’re on Next.js with the App Router and Vite is not part of your stack, stay with Jest. If you’re on any Vite-based framework, the migration is worth the 30-60 minutes it takes.
Sponsored
More from this category
More from Technology
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored