| name | vitest-testing-patterns |
| description | Write tests using Vitest and React Testing Library. Use when creating unit tests, component tests, integration tests, or mocking dependencies. Activates for test file creation, mock patterns, coverage, and testing best practices. |
| allowed-tools | Read,Write,Edit,Bash(npm:*,npx:*) |
| metadata | {"category":"Code Quality & Testing","tags":["testing","code","automation","jest","react"],"pairs-with":[{"skill":"test-automation-expert","reason":"Vitest unit testing is one layer in a comprehensive test automation strategy"},{"skill":"playwright-e2e-tester","reason":"Unit tests (Vitest) and E2E tests (Playwright) form complementary test pyramid layers"},{"skill":"react-performance-optimizer","reason":"Component test patterns verify that performance optimizations preserve correct behavior"},{"skill":"typescript-advanced-patterns","reason":"Type-safe test utilities and mock factories leverage advanced TypeScript patterns"}]} |
Vitest Testing Patterns
This skill helps you write effective tests using Vitest and React Testing Library following project conventions.
When to Use
✅ USE this skill for:
- Writing unit tests for utilities and functions
- Creating component tests with React Testing Library
- Setting up mocks for API calls, databases, or external services
- Integration testing patterns
- Understanding test coverage and CI setup
❌ DO NOT use for:
- Jest-specific patterns → similar but check Jest docs for differences
- End-to-end testing → use Playwright or Cypress skills
- Performance testing → use dedicated performance tools
- API contract testing → use OpenAPI/Pact patterns
Test Infrastructure
Configuration: vitest.config.ts
- Environment: jsdom
- Setup file:
src/test/setup.ts
- Coverage: v8 provider
Commands:
npm test
npm run test:run
npm run test:coverage
File Organization
src/
├── app/api/__tests__/ # API route tests
├── components/__tests__/ # Component tests
├── lib/__tests__/ # Library/utility tests
└── lib/{feature}/__tests__/ # Feature-specific tests
Name tests as {name}.test.ts or {name}.test.tsx.
Core Testing Patterns
1. API Route Tests
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST } from '../route';
import { NextRequest } from 'next/server';
vi.mock('@/lib/auth', () => ({
getSession: vi.fn(),
}));
vi.mock('@/db', () => ({
db: {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([]),
},
}));
describe('GET /api/feature', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns 401 when not authenticated', async () => {
vi.mocked(getSession).mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/feature');
const response = await (request);
(response.).();
});
(, () => {
vi.(getSession).({ : });
vi.(db.).({
: vi.().({
: vi.().([{ : , : }]),
}),
});
request = ();
response = (request);
data = response.();
(response.).();
(data).();
});
});
2. Component Tests
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FeatureComponent } from '../FeatureComponent';
vi.mock('@/hooks/useAuth', () => ({
useAuth: vi.fn().mockReturnValue({
user: { id: 'user-123', name: 'Test User' },
isLoading: false,
}),
}));
describe('FeatureComponent', () => {
it('renders loading state', () => {
vi.mocked(useAuth).mockReturnValueOnce({
user: null,
isLoading: true,
});
render(<FeatureComponent />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('handles user interaction', async () => {
user = userEvent.();
onSubmit = vi.();
();
user.(screen.(), );
user.(screen.(, { : }));
(onSubmit).();
});
(, () => {
vi.(fetch).( ());
();
( {
(screen.()).();
});
});
});
3. Library/Utility Tests
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { processData, formatDate } from '../utils';
describe('processData', () => {
it('transforms input correctly', () => {
const input = { raw: 'data' };
const result = processData(input);
expect(result).toEqual({
processed: true,
data: 'DATA',
});
});
it('throws on invalid input', () => {
expect(() => processData(null)).toThrow('Invalid input');
});
});
describe('formatDate', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-01-15T10:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('formats relative dates', () => {
const yesterday = ();
((yesterday)).();
});
});
Mocking Patterns
Module Mocking
vi.mock('@/lib/auth', () => ({
getSession: vi.fn(),
requireAuth: vi.fn(),
}));
vi.mock('date-fns', async () => {
const actual = await vi.importActual('date-fns');
return {
...actual,
format: vi.fn(() => '2025-01-15'),
};
});
vi.mock('@anthropic-ai/sdk', () => ({
default: class MockAnthropic {
messages = {
create: vi.fn().mockResolvedValue({
content: [{ type: 'text', text: 'Mock response' }],
usage: { input_tokens: 10, output_tokens: 20 },
}),
};
},
}));
Function Mocking
const mockFn = vi.fn();
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('Failed'));
mockFn.mockReturnValueOnce('first call only');
mockFn.mockImplementation((arg) => arg.toUpperCase());
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('expected', 'args');
Chained Mock Pattern (Drizzle ORM)
vi.mock('@/db', () => ({
db: {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue([{ id: '1' }]),
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([{ id: 'new-1' }]),
}),
}),
},
}));
Timer Mocking
describe('debounced function', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('debounces calls', async () => {
const callback = vi.fn();
const debounced = debounce(callback, 300);
debounced();
debounced();
debounced();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledTimes(1);
});
});
Query Priorities
Use queries in this order (most to least preferred):
- getByRole - Accessible queries (buttons, links, headings)
- getByLabelText - Form fields with labels
- getByPlaceholderText - Inputs with placeholders
- getByText - Non-interactive elements
- getByTestId - Last resort (data-testid)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByLabelText(/email/i);
screen.getByTestId('submit-button');
Async Patterns
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
const element = await screen.findByText('Loaded');
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
Test Cleanup
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup();
vi.clearAllMocks();
vi.resetAllMocks();
vi.restoreAllMocks();
});
Accessibility Testing
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('has no accessibility violations', async () => {
const { container } = render(<Component />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Common Matchers
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toHaveTextContent('text');
expect(element).toHaveAttribute('href', '/path');
expect(element).toHaveClass('active');
expect(input).toHaveValue('input value');
References