vitest-testing-patterns

安装量: 122
排名: #7052

安装

npx skills add https://github.com/erichowens/some_claude_skills --skill vitest-testing-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 # 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);

}); });

  1. 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);
});

}); });

  1. 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(); 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 Vitest Mocking Guide React Testing Library Testing Library Queries

返回排行榜