Testing Library Core Knowledge
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:testing-libraryfor comprehensive documentation.
When NOT to Use This Skill
- E2E Testing - Use
playwrightorcypressfor full browser automation - Unit Testing - Use
vitestorjestfor isolated function tests - API Testing - Use framework-specific HTTP testing tools
- Performance Testing - Use Lighthouse or dedicated performance tools
- Non-UI Logic - Test pure functions with unit tests instead
Basic Test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserForm } from './UserForm';
test('submits form with user data', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/name/i), 'John');
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
name: 'John',
email: 'john@example.com',
});
});
Queries (Priority Order)
// 1. Accessible (preferred)
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText(/email/i);
screen.getByPlaceholderText(/search/i);
screen.getByText(/welcome/i);
screen.getByAltText(/logo/i);
screen.getByTitle(/close/i);
// 2. Test IDs (fallback)
screen.getByTestId('submit-button');
// Query variants
getBy... // Throws if not found
queryBy... // Returns null if not found
findBy... // Async, waits for element
getAllBy... // Returns array
User Events
const user = userEvent.setup();
await user.click(element);
await user.dblClick(element);
await user.type(input, 'text');
await user.clear(input);
await user.selectOptions(select, 'option1');
await user.keyboard('{Enter}');
await user.hover(element);
await user.tab();
Async Utilities
import { waitFor, waitForElementToBeRemoved } from '@testing-library/react';
// Wait for condition
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
// Wait for element removal
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));
// findBy is shorthand for waitFor + getBy
const element = await screen.findByText(/loaded/i);
Custom Render
// test-utils.tsx
function renderWithProviders(ui: React.ReactElement) {
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider>
{ui}
</ThemeProvider>
</QueryClientProvider>
);
}
export * from '@testing-library/react';
export { renderWithProviders as render };
Production Readiness
Test Organization
// test-utils.tsx - Centralized test setup
import { render, RenderOptions } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
interface WrapperProps {
children: React.ReactNode;
}
function AllTheProviders({ children }: WrapperProps) {
const queryClient = createTestQueryClient();
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<RouterProvider>
{children}
</RouterProvider>
</ThemeProvider>
</QueryClientProvider>
);
}
const customRender = (
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => ({
user: userEvent.setup(),
...render(ui, { wrapper: AllTheProviders, ...options }),
});
export * from '@testing-library/react';
export { customRender as render };
Accessibility Testing
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('form should be accessible', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
// Test keyboard navigation
test('should be keyboard navigable', async () => {
const { user } = render(<Navigation />);
await user.tab();
expect(screen.getByRole('link', { name: /home/i })).toHaveFocus();
await user.tab();
expect(screen.getByRole('link', { name: /about/i })).toHaveFocus();
});
MSW Integration
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John' },
]);
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 2, ...body }, { status: 201 });
}),
];
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// setup.ts
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// In tests
test('displays users from API', async () => {
render(<UserList />);
expect(await screen.findByText('John')).toBeInTheDocument();
});
Testing Patterns
// Test loading states
test('shows loading spinner while fetching', async () => {
render(<UserProfile userId="1" />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(screen.getByText('John')).toBeInTheDocument();
});
// Test error states
test('shows error message on failure', async () => {
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
})
);
render(<UserProfile userId="999" />);
expect(await screen.findByRole('alert')).toHaveTextContent('Not found');
});
// Test form validation
test('shows validation errors', async () => {
const { user } = render(<SignupForm />);
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
Monitoring Metrics
| Metric | Target | |--------|--------| | Component test coverage | > 80% | | Accessibility violations | 0 | | Test execution time | < 30s | | Flaky test rate | 0% |
Checklist
- [ ] Custom render with providers
- [ ] userEvent.setup() for interactions
- [ ] Accessible queries (getByRole, getByLabelText)
- [ ] Accessibility testing with jest-axe
- [ ] MSW for API mocking
- [ ] Loading/error state tests
- [ ] Keyboard navigation tests
- [ ] Form validation tests
- [ ] No implementation details tested
- [ ] Async operations properly awaited
Anti-Patterns
| Anti-Pattern | Why It's Bad | Solution | |--------------|--------------|----------| | Using container.querySelector() | Tests implementation, not behavior | Use screen.getByRole, getByLabelText | | Testing state directly | Brittle, coupled to implementation | Test rendered output, user interactions | | Using fireEvent over userEvent | Less realistic user interactions | Use userEvent for better simulation | | Not waiting for async updates | Flaky tests, false failures | Use findBy*, waitFor, or act() | | Testing CSS classes | Coupled to styling | Test visual behavior or aria attributes | | Multiple assertions without findBy | Race conditions | Use findBy for elements that appear async | | Not using data-testid sparingly | Defeats accessibility purpose | Prefer getByRole, use testid as fallback |
Quick Troubleshooting
| Problem | Likely Cause | Solution | |---------|--------------|----------| | "Unable to find element" | Wrong query or element not rendered | Use screen.debug(), check query | | "Not wrapped in act(...)" | State update not awaited | Use findBy* or waitFor | | "Unable to find accessible element" | Missing label or role | Add aria-label, role, or label element | | Test timeout | Async operation not resolving | Check findBy timeout, verify API mock | | "Multiple elements found" | Non-specific query | Use more specific query or { name } option | | fireEvent doesn't trigger handler | Wrong event or React version | Use userEvent instead of fireEvent |
微信扫一扫