vitest-testing-patterns▌
erichowens/some_claude_skills · updated Apr 8, 2026
This skill helps you write effective tests using Vitest and React Testing Library following project conventions.
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 # Watch mode
npm run test:run # Single run
npm run test:coverage # With 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';
// Mock dependencies
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 GET(request);
expect(response.status).toBe(401);
});
it('returns data when authenticated', async () => {
vi.mocked(getSession).mockResolvedValue({ userId: 'user-123' });
vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
}),
});
const request = new NextRequest('http://localhost/api/feature');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(1);
});
});
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';
// Mock hooks
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 () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<FeatureComponent onSubmit={onSubmit} />);
await user.type(screen.getByRole('textbox'), 'Test input');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith('Test input');
});
it('displays error state', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
render(<FeatureComponent />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
});
});
});
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 = new Date('2025-01-14T10:00:00Z');
expect(formatDate(yesterday)).toBe('yesterday');
});
});
Mocking Patterns
Module Mocking
// Mock entire module
vi.mock('@/lib/auth', () => ({
getSession: vi.fn(),
requireAuth: vi.fn(),
}));
// Mock with partial implementation
vi.mock('date-fns', async () => {
const actual = await vi.importActual('date-fns');
return {
...actual,
format: vi.fn(() => '2025-01-15'),
};
});
// Mock default export (like Anthropic SDK)
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
// Create mock function
const mockFn = vi.fn();
// Set return values
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('Failed'));
// One-time behavior
mockFn.mockReturnValueOnce('first call only');
// Custom implementation
mockFn.mockImplementation((arg) => arg.toUpperCase());
// Verify calls
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)
// Preferred
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByLabelText(/email/i);
// Avoid unless necessary
screen.getByTestId('submit-button');
Async Patterns
// Wait for element to appear
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
// Find (built-in waitFor)
const element = await screen.findByText('Loaded');
// Wait for element to disappear
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
Test Cleanup
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup(); // React cleanup (automatic with setup.ts)
vi.clearAllMocks(); // Reset mock call counts
vi.resetAllMocks(); // Reset mocks to initial state
vi.restoreAllMocks(); // Restore original implementations
});
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
// jest-dom matchers (from setup.ts)
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
Discussion
Product Hunt–style comments (not star reviews)- No comments yet — start the thread.
Ratings
4.6★★★★★31 reviews- ★★★★★Aanya Diallo· Dec 16, 2024
I recommend vitest-testing-patterns for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
- ★★★★★Sakshi Patil· Nov 19, 2024
vitest-testing-patterns fits our agent workflows well — practical, well scoped, and easy to wire into existing repos.
- ★★★★★Aisha Abbas· Nov 7, 2024
Useful defaults in vitest-testing-patterns — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
- ★★★★★Hassan Dixit· Oct 26, 2024
Registry listing for vitest-testing-patterns matched our evaluation — installs cleanly and behaves as described in the markdown.
- ★★★★★Chaitanya Patil· Oct 10, 2024
vitest-testing-patterns has been reliable in day-to-day use. Documentation quality is above average for community skills.
- ★★★★★Oshnikdeep· Sep 25, 2024
Useful defaults in vitest-testing-patterns — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
- ★★★★★Kabir Martin· Sep 17, 2024
I recommend vitest-testing-patterns for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
- ★★★★★Aditi Reddy· Sep 5, 2024
vitest-testing-patterns reduced setup friction for our internal harness; good balance of opinion and flexibility.
- ★★★★★Aditi Jain· Aug 24, 2024
vitest-testing-patterns is among the better-maintained entries we tried; worth keeping pinned for repeat workflows.
- ★★★★★Ganesh Mohane· Aug 16, 2024
Registry listing for vitest-testing-patterns matched our evaluation — installs cleanly and behaves as described in the markdown.
showing 1-10 of 31