Vitest Testing Skill
You are an expert software engineer specializing in testing with Vitest. When the user asks you to write, review, or debug Vitest tests, follow these detailed instructions.
Core Principles
- Blazing fast -- Vitest is designed for speed with native ESM support and smart test running.
- Vite-native -- Leverages Vite's config, transformers, and plugins for seamless integration.
- Jest-compatible API -- Familiar API makes migration from Jest straightforward.
- Watch mode first -- Vitest excels at watch mode with instant feedback.
- Test isolation -- Each test should be independent and deterministic.
Project Structure
project/
src/
components/
Button.tsx
Button.test.tsx
services/
user.service.ts
user.service.test.ts
utils/
validators.ts
validators.test.ts
tests/
integration/
api.test.ts
db.test.ts
fixtures/
test-data.ts
setup.ts
vitest.config.ts
vite.config.ts
Configuration
Basic Config
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.test.ts',
'**/*.spec.ts',
'**/types/',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'build'],
testTimeout: 10000,
hookTimeout: 10000,
},
});
Advanced Config with Multiple Environments
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
environmentMatchGlobs: [
['**/*.test.tsx', 'jsdom'],
['**/*.browser.test.ts', 'jsdom'],
['**/*.node.test.ts', 'node'],
['**/*.edge.test.ts', 'edge-runtime'],
],
poolOptions: {
threads: {
singleThread: false,
isolate: true,
},
},
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
},
benchmark: {
include: ['**/*.bench.{ts,tsx}'],
},
},
});
Writing Tests
Basic Test Structure
typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { sum, multiply } from './math';
describe('Math utilities', () => {
it('should add two numbers', () => {
expect(sum(2, 3)).toBe(5);
});
it('should multiply two numbers', () => {
expect(multiply(4, 5)).toBe(20);
});
it('should handle zero', () => {
expect(sum(0, 0)).toBe(0);
expect(multiply(5, 0)).toBe(0);
});
it('should handle negative numbers', () => {
expect(sum(-1, 1)).toBe(0);
expect(multiply(-2, 3)).toBe(-6);
});
});
Testing Classes and Services
typescript
// user.service.ts
export class UserService {
constructor(
private apiClient: ApiClient,
private cache: Cache
) {}
async getUser(id: string): Promise<User> {
const cached = await this.cache.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await this.apiClient.get(`/users/${id}`);
await this.cache.set(`user:${id}`, JSON.stringify(user), 3600);
return user;
}
async createUser(data: CreateUserDto): Promise<User> {
return this.apiClient.post('/users', data);
}
}
typescript
// user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './user.service';
describe('UserService', () => {
let userService: UserService;
let mockApiClient: any;
let mockCache: any;
beforeEach(() => {
mockApiClient = {
get: vi.fn(),
post: vi.fn(),
};
mockCache = {
get: vi.fn(),
set: vi.fn(),
};
userService = new UserService(mockApiClient, mockCache);
});
describe('getUser', () => {
it('should return cached user if available', async () => {
const cachedUser = { id: '1', name: 'Cached User' };
mockCache.get.mockResolvedValue(JSON.stringify(cachedUser));
const result = await userService.getUser('1');
expect(result).toEqual(cachedUser);
expect(mockCache.get).toHaveBeenCalledWith('user:1');
expect(mockApiClient.get).not.toHaveBeenCalled();
});
it('should fetch user from API if not cached', async () => {
const user = { id: '1', name: 'API User' };
mockCache.get.mockResolvedValue(null);
mockApiClient.get.mockResolvedValue(user);
const result = await userService.getUser('1');
expect(result).toEqual(user);
expect(mockApiClient.get).toHaveBeenCalledWith('/users/1');
expect(mockCache.set).toHaveBeenCalledWith(
'user:1',
JSON.stringify(user),
3600
);
});
});
describe('createUser', () => {
it('should create a new user', async () => {
const newUser = { id: '2', name: 'New User', email: 'new@example.com' };
mockApiClient.post.mockResolvedValue(newUser);
const result = await userService.createUser({
name: 'New User',
email: 'new@example.com',
});
expect(result).toEqual(newUser);
expect(mockApiClient.post).toHaveBeenCalledWith('/users', {
name: 'New User',
email: 'new@example.com',
});
});
});
});
Mocking Patterns
Function Mocking
typescript
import { vi, describe, it, expect } from 'vitest';
describe('Function mocking', () => {
it('should mock a function', () => {
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn).toHaveBeenCalledOnce();
});
it('should mock implementation', () => {
const mockFn = vi.fn((x: number) => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
});
it('should mock resolved value', async () => {
const mockFn = vi.fn();
mockFn.mockResolvedValue({ id: 1, name: 'Test' });
const result = await mockFn();
expect(result).toEqual({ id: 1, name: 'Test' });
});
it('should mock rejected value', async () => {
const mockFn = vi.fn();
mockFn.mockRejectedValue(new Error('Failed'));
await expect(mockFn()).rejects.toThrow('Failed');
});
});
Module Mocking
typescript
// __mocks__/axios.ts
import { vi } from 'vitest';
export default {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
create: vi.fn(() => ({
get: vi.fn(),
post: vi.fn(),
})),
};
typescript
// api.test.ts
import { describe, it, expect, vi } from 'vitest';
vi.mock('axios');
import axios from 'axios';
import { fetchUser } from './api';
describe('API', () => {
it('should fetch user data', async () => {
const mockUser = { id: 1, name: 'Test User' };
vi.mocked(axios.get).mockResolvedValue({ data: mockUser });
const user = await fetchUser('1');
expect(user).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
});
Partial Module Mocking
typescript
import { vi } from 'vitest';
// Mock only specific exports
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils');
return {
...actual,
fetchData: vi.fn(),
};
});
Spying
typescript
import { vi, describe, it, expect } from 'vitest';
describe('Spying', () => {
it('should spy on console.error', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
console.error('Test error');
expect(spy).toHaveBeenCalledWith('Test error');
spy.mockRestore();
});
it('should spy on object method', () => {
const obj = {
method: (x: number) => x * 2,
};
const spy = vi.spyOn(obj, 'method');
obj.method(5);
expect(spy).toHaveBeenCalledWith(5);
expect(spy).toHaveReturnedWith(10);
});
});
Timer Mocking
typescript
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Timer tests', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should debounce function calls', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
it('should throttle function calls', () => {
const fn = vi.fn();
const throttled = throttle(fn, 100);
throttled();
expect(fn).toHaveBeenCalledOnce();
throttled();
expect(fn).toHaveBeenCalledOnce(); // still once
vi.advanceTimersByTime(100);
throttled();
expect(fn).toHaveBeenCalledTimes(2);
});
it('should handle setTimeout', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledOnce();
});
});
Snapshot Testing
typescript
import { describe, it, expect } from 'vitest';
describe('Snapshot tests', () => {
it('should match snapshot', () => {
const data = {
id: 1,
name: 'Test User',
roles: ['admin', 'user'],
createdAt: new Date('2024-01-01'),
};
expect(data).toMatchSnapshot();
});
it('should match inline snapshot', () => {
const formatted = formatUserName({ first: 'John', last: 'Doe' });
expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});
it('should match file snapshot', () => {
const html = renderComponent({ title: 'Test', count: 5 });
expect(html).toMatchFileSnapshot('./__snapshots__/component.html');
});
});
Testing React Components
typescript
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button component', () => {
it('should render with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should handle click events', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(onClick).toHaveBeenCalledOnce();
});
it('should be disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('should apply variant classes', () => {
render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
});
Async Testing
typescript
import { describe, it, expect } from 'vitest';
describe('Async tests', () => {
it('should handle async/await', async () => {
const result = await fetchData();
expect(result.status).toBe('success');
});
it('should handle promises with resolves', async () => {
await expect(fetchUser('1')).resolves.toEqual({
id: '1',
name: 'Test User',
});
});
it('should handle promise rejections', async () => {
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});
it('should test multiple async operations', async () => {
const [user, posts] = await Promise.all([
fetchUser('1'),
fetchPosts('1'),
]);
expect(user.id).toBe('1');
expect(posts).toHaveLength(5);
});
});
Testing with Context and Fixtures
typescript
import { describe, it, expect, beforeEach } from 'vitest';
describe('User operations', () => {
let testUser: User;