by thetestingacademy
Modern JavaScript and TypeScript testing with Vitest, covering unit testing, integration testing, mocking, snapshots, browser mode, and Vite integration.
npx @qaskills/cli add vitestAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
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
// 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,
},
});
// 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}'],
},
},
});
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);
});
});
// 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);
}
}
// 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',
});
});
});
});
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');
});
});
// __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(),
})),
};
// 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');
});
});
import { vi } from 'vitest';
// Mock only specific exports
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils');
return {
...actual,
fetchData: vi.fn(),
};
});
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);
});
});
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();
});
});
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');
});
});
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');
});
});
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);
});
});
import { describe, it, expect, beforeEach } from 'vitest';
describe('User operations', () => {
let testUser: User;
beforeEach<{ user: User }>(async (context) => {
// Setup runs before each test
testUser = await createTestUser();
context.user = testUser;
});
it<{ user: User }>('should update user name', async ({ user }) => {
await updateUserName(user.id, 'New Name');
const updated = await getUser(user.id);
expect(updated.name).toBe('New Name');
});
it<{ user: User }>('should delete user', async ({ user }) => {
await deleteUser(user.id);
await expect(getUser(user.id)).rejects.toThrow('Not found');
});
});
// vitest.config.ts
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
headless: true,
},
},
});
// button.browser.test.ts
import { describe, it, expect } from 'vitest';
import { page } from '@vitest/browser/context';
describe('Button in real browser', () => {
it('should interact with button', async () => {
await page.goto('/button-demo');
const button = await page.locator('button');
await button.click();
const counter = await page.locator('#counter');
await expect(counter).toHaveText('1');
});
});
globals: true or explicit imports.*.test.ts files next to source files for quick access.test.skip or .only.# Run all tests
vitest
# Run in watch mode (default)
vitest watch
# Run once (CI mode)
vitest run
# Run specific file
vitest run src/utils/validators.test.ts
# Run with UI
vitest --ui
# Run with coverage
vitest --coverage
# Run tests matching pattern
vitest --reporter=verbose --grep="user"
# Run in browser mode
vitest --browser
# Run benchmarks
vitest bench
// Use test.only to isolate tests
it.only('should debug this test', () => {
// debugger; // Breakpoint
expect(true).toBe(true);
});
// Use console.log for quick debugging
it('should log values', () => {
const value = computeValue();
console.log('Computed value:', value);
expect(value).toBe(42);
});
// tests/setup.ts
import { expect } from 'vitest';
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
};
},
});
declare module 'vitest' {
interface Assertion<T = any> {
toBeWithinRange(floor: number, ceiling: number): T;
}
}
Vitest automatically uses your Vite config, including plugins like:
@vitejs/plugin-react for React JSX supportvite-tsconfig-paths for TypeScript path mappingThis makes Vitest the natural choice for Vite-based projects.
- name: Install QA Skills
run: npx @qaskills/cli add vitest10 of 29 agents supported