Testing React Components
Write effective unit and integration tests for React applications.
By EMEPublished: February 20, 2025
reacttestingjesttesting libraryunit tests
A Simple Analogy
Testing React components is like quality assurance for assembly parts. Each component is tested independently and in combination.
Why Component Tests?
- Regression prevention: Catch breaking changes
- Refactoring confidence: Safe code changes
- Documentation: Tests show expected behavior
- Maintainability: Early issue detection
- User experience: Test actual interactions
Jest Setup
// package.json
{
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@testing-library/jest-dom": "^6.1.0",
"jest": "^29.0.0"
}
}
// jest.config.js
export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
}
}
Component Testing
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click</Button>);
screen.getByText('Click').click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('disables when prop is true', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
User Events
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits form with valid input', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
Async Testing
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
describe('UserList', () => {
it('loads and displays users', async () => {
render(<UserList />);
// Component is loading
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
// Users displayed
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
});
Mocking Modules
import { render, screen } from '@testing-library/react';
import { ProductPage } from './ProductPage';
import * as api from '@/api/products';
jest.mock('@/api/products');
describe('ProductPage', () => {
beforeEach(() => {
jest.mocked(api.getProduct).mockResolvedValue({
id: '1',
name: 'Widget',
price: 29.99
});
});
it('displays product data', async () => {
render(<ProductPage productId="1" />);
expect(await screen.findByText('Widget')).toBeInTheDocument();
expect(screen.getByText('$29.99')).toBeInTheDocument();
});
});
Snapshot Testing
import { render } from '@testing-library/react';
import { Card } from './Card';
describe('Card', () => {
it('renders correctly', () => {
const { container } = render(
<Card title="Test">
<p>Content</p>
</Card>
);
expect(container).toMatchSnapshot();
});
});
Best Practices
- Test behavior: Not implementation
- Semantic queries: Use accessible queries
- User events: Simulate real interactions
- Avoid snapshots: Prefer specific assertions
- Test user flows: Integration tests matter
Related Concepts
- End-to-end testing
- Visual regression
- Performance testing
- Accessibility testing
Summary
Use React Testing Library to test components through user interactions rather than implementation details. Focus on what users see and do.