Comprehensive React component testing with Jest and React Testing Library covering configuration, mocking strategies, async testing patterns, hooks testing, and integration testing best practices
Comprehensive testing guide for React applications using Jest and React Testing Library
This skill provides a complete guide to testing React applications using Jest and React Testing Library. It covers everything from basic setup to advanced patterns, focusing on writing maintainable, user-centric tests that give you confidence in your application.
This skill follows the core principles of Testing Library:
Test Behavior, Not Implementation
Accessibility First
getByRole and getByLabelText over getByTestIdConfidence Over Coverage
Maintainable Tests
For React 18+ projects:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest jest-environment-jsdom
For TypeScript projects, also install:
npm install --save-dev @types/jest ts-jest
Create jest.config.js in your project root:
/** @type {import('jest').Config} */
const config = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
module.exports = config;
Create src/setupTests.js:
import '@testing-library/jest-dom';
Add test script to package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
import { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';
test('renders greeting message', () => {
render(<Greeting name="World" />);
expect(screen.getByText(/hello, world/i)).toBeInTheDocument();
});
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
test('increments counter on click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
test('loads and displays user data', async () => {
render(<UserProfile userId={1} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const username = await screen.findByText(/john doe/i);
expect(username).toBeInTheDocument();
});
Use queries in this order (most to least preferred):
getByRole - Most accessible, reflects how users find elements
screen.getByRole('button', { name: /submit/i })
getByLabelText - Good for form fields
screen.getByLabelText(/email/i)
getByPlaceholderText - For inputs with placeholders
screen.getByPlaceholderText(/search/i)
getByText - For non-interactive elements
screen.getByText(/welcome/i)
getByTestId - Last resort only
screen.getByTestId('custom-element')
// Element should exist
const button = screen.getByRole('button');
// Element might not exist
const error = screen.queryByText(/error/i);
expect(error).not.toBeInTheDocument();
// Element appears after async operation
const data = await screen.findByText(/loaded/i);
Always prefer userEvent over fireEvent:
// Good - simulates real user interactions
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(button);
await user.type(input, 'text');
// Avoid - lower-level, doesn't simulate real interactions
import { fireEvent } from '@testing-library/react';
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'text' } });
test('submits form with user data', async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<SignupForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign up/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
test('shows error when loading fails', async () => {
render(<DataComponent url="/api/fail" />);
const error = await screen.findByRole('alert');
expect(error).toHaveTextContent(/failed to load/i);
});
test('renders all items', () => {
const items = ['Apple', 'Banana', 'Cherry'];
render(<ItemList items={items} />);
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
});
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ name: 'John' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('calls onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({ name: 'John' })),
}));
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
import { MemoryRouter } from 'react-router-dom';
const renderWithRouter = (ui, { initialEntries = ['/'] } = {}) => {
return render(
<MemoryRouter initialEntries={initialEntries}>
{ui}
</MemoryRouter>
);
};
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
const createMockStore = (initialState) => {
return configureStore({
reducer: rootReducer,
preloadedState: initialState,
});
};
const renderWithStore = (ui, { store = createMockStore() } = {}) => {
return render(<Provider store={store}>{ui}</Provider>);
};
import { screen } from '@testing-library/react';
screen.debug(); // Prints entire DOM
screen.debug(element); // Prints specific element
screen.logTestingPlaygroundURL(); // Opens Testing Playground with current DOM
Can't find element:
screen.debug() to see DOMAct warnings:
Test timeout:
See EXAMPLES.md for 15+ comprehensive test examples covering:
This skill is designed to be comprehensive but approachable. If you find areas that need clarification or additional examples, contributions are welcome!
This skill documentation is provided as-is for educational purposes.
Version: 1.0.0 Last Updated: October 2025 Maintained by: Claude Skills Team
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer