Vitest 2.0: Fast Unit Testing for Modern JavaScript & TypeScript Projects
Vitest 2.0 brings major performance improvements, better ESM support, and native TypeScript testing. Learn how to migrate and leverage its speed for your test suite.
Introduction
Vitest 2.0 marks a significant milestone for the JavaScript testing ecosystem. As a blazingly-fast unit test framework powered by Vite, Vitest has rapidly become the go-to choice for developers building modern applications with TypeScript, Vue, React, and other frameworks. The 2.0 release doubles down on performance, introduces native TypeScript support without compilation overhead, and streamlines configuration for teams moving away from Jest.
If you’re still running Jest or an older testing setup, or if you’re curious about what makes Vitest stand out, this guide will walk you through the key features, migration paths, and practical examples to get you started.
Why Vitest 2.0 Matters
Speed as a First-Class Feature
Vitest leverages Vite’s instant module serving and hot module replacement (HMR) to eliminate the test compilation bottleneck that plagues Jest and other traditional test runners. In real-world projects, Vitest runs 5–10× faster than Jest on initial runs and offers near-instant re-runs during development.
Why? Jest was built for a different era. It compiles your entire test suite upfront using Babel, waits for the result, and then executes tests. Vitest’s architecture, built on Vite, only transforms what’s needed, when it’s needed.
Native TypeScript Support
Vitest 2.0 now includes native TypeScript transpilation without requiring ts-jest or similar plugins. This means:
-
No extra configuration for
.tsand.tsxtest files - Instant TypeScript parsing and execution
- Full type inference in test assertions
- No Babel setup required
ESM-First by Default
Vitest 2.0 fully embraces ES modules. CommonJS is still supported, but the framework prioritizes ESM, making it future-proof and aligned with the JavaScript ecosystem’s direction. This is especially valuable if you’re shipping ESM packages yourself.
Getting Started
Installation
Start by installing Vitest and its peer dependencies:
npm install -D vitest @vitest/ui happy-dom
Breakdown:
- vitest: The test runner itself
- @vitest/ui: Optional but highly recommended — a browser-based dashboard for test results
- happy-dom: A lightweight DOM implementation for component testing (or use jsdom if you need more comprehensive DOM APIs)
Basic Configuration
Create a vitest.config.ts (or vitest.config.js) at your project root:
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue'; // if using Vue
export default defineConfig({
plugins: [vue()],
test: {
globals: true, // enables describe/it/expect without imports
environment: 'happy-dom', // or 'jsdom' for fuller DOM support
setupFiles: ['./tests/setup.ts'], // optional: run before all tests
coverage: {
provider: 'v8', // built-in coverage
reporter: ['text', 'json', 'html'],
lines: 80,
functions: 80,
branches: 75,
},
},
});
With globals: true, you don’t need to import describe, it, expect, etc. into every test file — a nice quality-of-life improvement over Jest’s defaults.
Your First Test
Create src/math.ts:
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
Now create src/__tests__/math.test.ts:
import { describe, it, expect } from 'vitest';
import { add, multiply } from '../math';
describe('Math utilities', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('should multiply two numbers', () => {
expect(multiply(4, 5)).toBe(20);
});
it('should handle negative numbers', () => {
expect(add(-1, -1)).toBe(-2);
expect(multiply(-2, 3)).toBe(-6);
});
});
Run the tests:
npm run test
Vitest will instantly compile and run your TypeScript tests without any intermediate build step. If you installed @vitest/ui, you can also run:
npm run test -- --ui
This opens a browser-based dashboard at http://localhost:51204/__vitest__/ showing all test results in real-time.
Step-by-Step Guide: Migration from Jest
Step 1: Install Vitest
Install Vitest alongside Jest (don’t remove Jest yet):
npm install -D vitest happy-dom @vitest/ui
Step 2: Create vitest.config.ts
Most Jest configurations can be reused. Here’s a template that covers common Jest patterns:
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
alias: {
'@': path.resolve(__dirname, './src'),
},
setupFiles: ['./tests/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
});
Key differences from Jest:
-
testEnvironment→environment -
setupFilesAfterEnv→setupFiles(Vitest runs setup before imports) -
Module aliases go in Vite’s
resolve.alias
Step 3: Update package.json Scripts
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
Step 4: Replace Jest Globals (if applicable)
If you have a Jest setup file, some APIs have moved:
jest.mock() → Still supported, but Vitest prefers explicit imports
Instead of:
jest.mock('../api', () => ({
fetchUser: () => Promise.resolve({ id: 1 }),
}));
Use Vitest’s vi helper:
import { vi } from 'vitest';
vi.mock('../api', () => ({
fetchUser: () => Promise.resolve({ id: 1 }),
}));
jest.spyOn() → vi.spyOn()
const spy = vi.spyOn(obj, 'method');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
Step 5: Run Tests in Parallel
Vitest runs tests in parallel by default (unlike Jest). If you have tests with side effects or shared state, isolate them:
import { describe, it, expect } from 'vitest';
describe.sequential('Database tests', () => {
// These run sequentially
it('should insert a record', () => { /* ... */ });
it('should update the record', () => { /* ... */ });
});
Step 6: Gradual Adoption
You can run both Jest and Vitest in parallel during migration:
{
"scripts": {
"test:jest": "jest",
"test:vitest": "vitest run",
"test": "npm run test:jest && npm run test:vitest"
}
}
Once all tests pass in Vitest, remove Jest and its config files.
Advanced Patterns
Mocking and Spying
Vitest’s vi module provides comprehensive mocking utilities:
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as api from '../api';
describe('User Service', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch user data', async () => {
const mockUser = { id: 1, name: 'Alice' };
vi.spyOn(api, 'getUser').mockResolvedValue(mockUser);
const user = await api.getUser(1);
expect(user).toEqual(mockUser);
expect(api.getUser).toHaveBeenCalledWith(1);
});
it('should handle errors', async () => {
const error = new Error('Network error');
vi.spyOn(api, 'getUser').mockRejectedValue(error);
await expect(api.getUser(1)).rejects.toThrow('Network error');
});
});
Testing Async Code
Vitest handles promises, async/await, and callbacks seamlessly:
import { it, expect } from 'vitest';
it('should resolve with data', async () => {
const result = await fetchData();
expect(result).toBeDefined();
});
it('should reject with error', () => {
return expect(failingFetch()).rejects.toThrow();
});
it('should call callback', (done) => {
callbackFunction(() => {
expect(true).toBe(true);
done();
});
});
Snapshot Testing
Snapshots work identically to Jest:
it('should render component', () => {
const html = renderComponent();
expect(html).toMatchSnapshot();
});
Update snapshots with:
vitest --update
Code Coverage
Generate coverage reports without additional dependencies:
vitest run --coverage
Vitest uses V8 (the JavaScript engine’s native coverage tool) for fast, accurate coverage. Open coverage/index.html to browse results.
Common Pitfalls
1. Test Isolation Issues
Vitest runs tests in parallel by default. If your tests modify global state (e.g., environment variables), use beforeEach and afterEach to clean up:
beforeEach(() => {
process.env.API_KEY = 'test-key';
});
afterEach(() => {
delete process.env.API_KEY;
});
2. Module Mocking Path Resolution
When mocking modules, ensure the path matches exactly how the module is imported:
// ❌ Wrong
vi.mock('api'); // if imported as import * as api from './api'
// ✅ Correct
vi.mock('./api');
3. Missing Environment Setup
If testing DOM-dependent code, ensure your config specifies an environment:
test: {
environment: 'happy-dom', // or 'jsdom'
},
Without this, document and window will be undefined.
4. Skipping Slow Tests in Development
Use it.skip() to temporarily disable slow tests during development:
it.skip('should process large dataset', () => {
// This is slow; skip it locally
});
Or run only a specific test:
vitest --grep "specific test name"
5. Type Issues with Global Test API
If using globals: true but not getting autocomplete, ensure your tsconfig.json includes Vitest types:
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Performance Tips
1. Use happy-dom for Most Tests
If you don’t need full HTML5 compliance, happy-dom is 2–3× faster than jsdom. Switch only components that truly need jsdom:
import { describe, it } from 'vitest';
describe.configure({ environment: 'jsdom' });
// All tests in this block use jsdom
2. Disable Coverage During Development
Coverage collection adds overhead. Enable it only in CI or on-demand:
# Development (fast)
vitest
# Coverage report (slower)
vitest run --coverage
3. Use Workspace Projects for Monorepos
If you have multiple packages, configure Vitest workspaces:
export default defineConfig({
test: {
workspace: [
{ root: './packages/core' },
{ root: './packages/utils' },
],
},
});
Practical Example: Testing an API Client
Here’s a real-world scenario — testing an authenticated API client:
// src/api-client.ts
interface FetchOptions {
headers?: Record<string, string>;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
}
export class ApiClient {
constructor(private baseUrl: string, private token: string) {}
async request<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
getUser(id: number) {
return this.request(`/users/${id}`);
}
}
Now, the test file:
// src/__tests__/api-client.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ApiClient } from '../api-client';
describe('ApiClient', () => {
let client: ApiClient;
const mockFetch = vi.fn();
beforeEach(() => {
global.fetch = mockFetch;
client = new ApiClient('https://api.example.com', 'test-token');
vi.clearAllMocks();
});
it('should include authorization header', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'Alice' }),
});
await client.getUser(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/users/1',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer test-token',
}),
}),
);
});
it('should throw on non-200 response', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
});
await expect(client.getUser(999)).rejects.toThrow('HTTP 404');
});
it('should parse JSON response', async () => {
const mockUser = { id: 1, name: 'Alice', email: '[email protected]' };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const user = await client.getUser(1);
expect(user).toEqual(mockUser);
});
});
Run with:
npm run test -- --ui
You’ll see all three tests pass instantly, with real-time re-runs as you edit.
Testing JSON Data with Kloubot Tools
When testing APIs that return JSON, you can use JSON Formatter to validate and pretty-print test fixtures before hardcoding them into your tests. For complex test data, Mock Data Generator can generate realistic fixtures matching your API schema, then paste them directly into your test files.
If your tests involve decoding JWTs returned from authentication endpoints, JWT Decoder lets you inspect token claims during development.
Debugging Tests
Vitest integrates seamlessly with Node debugging tools. Run tests with Inspector:
node --inspect-brk ./node_modules/vitest/vitest.mjs run
Then open chrome://inspect in Chrome DevTools to set breakpoints and step through test code.
Or use the WebStorm/VS Code debugger directly by adding a debug configuration.
Why It Matters for Your Team
- Faster Feedback Loop: Test execution in milliseconds vs. seconds accelerates development
- Less Configuration: Native TypeScript support eliminates ts-jest, babel-jest complexity
- Better DX: Hot module reload for tests, cleaner error messages, built-in UI dashboard
- Future-Proof: ESM-first design aligns with JavaScript ecosystem trends
- Drop-in Jest Replacement: If you use Jest’s API, migration takes hours, not days
Conclusion
Vitest 2.0 represents a maturation of modern JavaScript tooling. By building on Vite’s proven architecture and embracing TypeScript natively, it solves real pain points in the testing workflow. Whether you’re migrating from Jest, starting a new project, or optimizing your test suite’s performance, Vitest is worth serious consideration.
The ecosystem around Vitest continues to grow—with community plugins for Testing Library, MSW, and more—making it a safe bet for production applications across frameworks.
Start with the configuration template in this guide, run your first test, and experience the speed boost firsthand. You can also use API Request Builder to test your API endpoints during development, and Regex Tester if you’re validating patterns in your test assertions.