---
name: testing-strategies
description: Comprehensive testing strategies with Vitest, Jest, and Testing Library
license: MIT
compatibility: vitest 1+, jest 29+, testing-library/react 14+
allowed-tools: read_file write_file apply_patch search_with_context run_command
---
# Testing Strategies
## Testing Pyramid
```
/\
/ \ E2E Tests (few)
/----\ Integration Tests (some)
/ \ Unit Tests (many)
/________\
```
1. **Unit Tests** - Test isolated functions/components
2. **Integration Tests** - Test modules working together
3. **E2E Tests** - Test full user flows
## Vitest Configuration
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'tests/'],
},
},
});
```
## Unit Testing Functions
```typescript
// utils/format.ts
export function formatCurrency(amount: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
}
// utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './format';
describe('formatCurrency', () => {
it('formats USD by default', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
});
it('handles zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('supports other currencies', () => {
expect(formatCurrency(1000, 'EUR')).toBe('€1,000.00');
});
it('handles negative amounts', () => {
expect(formatCurrency(-50)).toBe('-$50.00');
});
});
```
## React Component Testing
```typescript
// Button.tsx
interface ButtonProps {
onClick: () => void;
disabled?: boolean;
children: React.ReactNode;
}
export function Button({ onClick, disabled, children }: ButtonProps) {
return (
);
}
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { vi, describe, it, expect } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders children', () => {
render();
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render();
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', () => {
const handleClick = vi.fn();
render();
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
});
```
## Testing Hooks
```typescript
// useCounter.ts
import { useState, useCallback } from 'react';
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount(c => c + 1), []);
const decrement = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, increment, decrement, reset };
}
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('starts with initial value', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
```
## Mocking
```typescript
// API mocking
import { vi } from 'vitest';
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
}));
// Module mocking
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
}),
}));
// Timer mocking
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
vi.useRealTimers();
```
## Async Testing
```typescript
import { waitFor, screen } from '@testing-library/react';
it('loads and displays user', async () => {
render();
// Wait for loading to complete
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
it('handles error state', async () => {
vi.mocked(fetchUser).mockRejectedValueOnce(new Error('Not found'));
render();
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Error loading user');
});
});
```
## Integration Testing APIs
```typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from '../src/app';
import { db } from '../src/db';
describe('POST /api/users', () => {
beforeAll(async () => {
await db.migrate.latest();
});
afterAll(async () => {
await db.destroy();
});
it('creates a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test User' })
.expect(201);
expect(response.body.data).toMatchObject({
email: 'test@example.com',
name: 'Test User',
});
});
it('returns 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'invalid', name: 'Test' })
.expect(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
});
```
## Test Organization
```
tests/
├── unit/ # Pure function tests
├── integration/ # API/DB tests
├── e2e/ # Full flow tests
├── fixtures/ # Test data
├── mocks/ # Mock implementations
└── setup.ts # Global test setup
```
## Best Practices
1. **Follow AAA pattern** - Arrange, Act, Assert
2. **One assertion per test** when possible
3. **Test behavior, not implementation**
4. **Use descriptive test names** that explain the scenario
5. **Keep tests isolated** - no shared state
6. **Mock external dependencies** but not the code under test
7. **Aim for 80%+ coverage** but prioritize critical paths