| name | unit-test |
| description | Unit tests with Jest + React Testing Library. CRITICAL - Uses Jest (NOT Vitest). |
Unit Test (Jest + Testing Library)
⚠️ JEST ONLY - NOT Vitest
jest.fn(), jest.mock(), jest.clearAllMocks()
import { vi } from 'vitest'
⭐⭐⭐ MINIMALIST: 1-2 TESTS PER COMPONENT ⭐⭐⭐
Rule: ONE comprehensive test > MANY brittle tests
| Component Type | Max Tests | Focus |
|---|
| Static (no fetch) | 1 | Renders with props |
| Async (with fetch) | 2 | Heading + loaded data |
DO NOT TEST (will cause failures):
- Empty state
- Error state
- Loading skeletons
- Each element separately
- Elements that may not exist
Combine multiple assertions in ONE test:
it('renders correctly', async () => {
expect(heading).toBeInTheDocument();
expect(item1).toBeInTheDocument();
expect(item2).toBeInTheDocument();
});
⭐ TEMPLATE (1-2 tests per component)
import { render, screen, waitFor, act } from '@testing-library/react';
import { MySection } from '@/components/home/MySection';
const originalError = console.error;
beforeAll(() => {
console.error = (...args: any[]) => {
if (args[0]?.includes?.('React does not recognize')) return;
if (args[0]?.includes?.('received `true` for a non-boolean')) return;
originalError(...args);
};
});
afterAll(() => {
console.error = originalError;
});
describe('MySection', () => {
beforeEach(() => {
jest.clearAllMocks();
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({
success: true,
data: [
{ id: '1', title: 'Item 1', author: 'Author 1' },
{ id: '2', title: 'Item 2', author: 'Author 2' },
],
}),
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('renders section with heading and content', async () => {
await act(async () => {
render(<MySection />);
});
expect(screen.getByRole('heading', { name: /my section/i })).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/item 1/i)).toBeInTheDocument();
});
expect(screen.getByText(/item 2/i)).toBeInTheDocument();
});
it('renders navigation link', async () => {
await act(async () => {
render(<MySection />);
});
const link = screen.getByRole('link', { name: /view all/i });
expect(link.getAttribute('href')).toContain('/items');
});
});
⛔ CRITICAL RULES
1. ASYNC: Use act() + waitFor()
await act(async () => { render(<Component />); });
await waitFor(() => { expect(screen.getByText(/data/i)).toBeInTheDocument(); });
2. MULTIPLE ELEMENTS: Use getAllByText() or getByRole()
const elements = screen.getAllByText(/science/i);
expect(elements.length).toBeGreaterThan(0);
expect(screen.getByRole('heading', { name: /science/i })).toBeInTheDocument();
3. CASE-INSENSITIVE: Always use /i flag
screen.getByText(/featured books/i);
screen.getByText('Featured Books');
4. LINKS: Flexible href matching
expect(link.getAttribute('href')).toContain('/books');
expect(link).toHaveAttribute('href', '/books');
5. ONLY TEST WHAT EXISTS IN SOURCE CODE
expect(screen.getByRole('heading')).toBeInTheDocument();
expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
🔧 MOCK FETCH
beforeEach(() => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: [...] }),
});
});
QUERY PRIORITY
| Priority | Query | Use Case |
|---|
| 1 | getByRole | Buttons, links, headings |
| 2 | getByText | Static text (use /i) |
| 3 | getAllByText | Text appears multiple times |
ANTI-PATTERNS
| Don't | Why |
|---|
| 5+ tests per component | Too many failure points |
| Test empty/error states | Component may not have them |
| Test loading skeletons | Data loads sync in Jest |
| Separate test per element | Combine in one test |
import { vi } from 'vitest' | Use Jest, not Vitest |