Table of Contents
Testing React Applications
Learn comprehensive testing strategies for React applications using Jest, React Testing Library, and modern testing practices.
Setting Up Testing Environment
// package.json dependencies
{
"devDependencies": {
"@testing-library/react": "^13.0.0",
"@testing-library/jest-dom": "^5.16.0",
"@testing-library/user-event": "^14.0.0",
"jest": "^27.0.0"
}
}
// setupTests.js
import '@testing-library/jest-dom';
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
moduleNameMapping: {
'^@/(.*)$': '/src/$1'
}
};
Basic Component Testing
// Button.js
function Button({ children, onClick, disabled = false, variant = 'primary' }) {
return (
);
}
// Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';
describe('Button Component', () => {
test('renders button with text', () => {
render();
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
test('calls onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render();
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('is disabled when disabled prop is true', () => {
render();
expect(screen.getByRole('button')).toBeDisabled();
});
test('applies correct CSS class for variant', () => {
render();
expect(screen.getByRole('button')).toHaveClass('btn-secondary');
});
});
Testing Components with State
// Counter.js
import { useState } from 'react';
function Counter({ initialValue = 0 }) {
const [count, setCount] = useState(initialValue);
return (
Count: {count}
);
}
// Counter.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
describe('Counter Component', () => {
test('displays initial count', () => {
render( );
expect(screen.getByTestId('count')).toHaveTextContent('Count: 5');
});
test('increments count when increment button is clicked', async () => {
const user = userEvent.setup();
render( );
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByTestId('count')).toHaveTextContent('Count: 1');
});
test('decrements count when decrement button is clicked', async () => {
const user = userEvent.setup();
render( );
await user.click(screen.getByRole('button', { name: /decrement/i }));
expect(screen.getByTestId('count')).toHaveTextContent('Count: 4');
});
test('resets count to zero when reset button is clicked', async () => {
const user = userEvent.setup();
render( );
await user.click(screen.getByRole('button', { name: /reset/i }));
expect(screen.getByTestId('count')).toHaveTextContent('Count: 0');
});
});
Testing Forms and User Input
// LoginForm.js
import { useState } from 'react';
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = {};
if (!email) newErrors.email = 'Email is required';
if (!password) newErrors.password = 'Password is required';
if (password.length < 6) newErrors.password = 'Password must be at least 6 characters';
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
onSubmit({ email, password });
}
};
return (
);
}
// LoginForm.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm Component', () => {
test('submits form with valid data', async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render( );
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: /login/i }));
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
test('shows validation errors for empty fields', async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render( );
await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByRole('alert', { name: /email is required/i })).toBeInTheDocument();
expect(screen.getByRole('alert', { name: /password is required/i })).toBeInTheDocument();
expect(mockSubmit).not.toHaveBeenCalled();
});
});
Testing Components with Context
// test-utils.js - Custom render function
import { render } from '@testing-library/react';
import { ThemeProvider } from '../contexts/ThemeContext';
const AllTheProviders = ({ children }) => {
return (
{children}
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };
// ThemedButton.test.js
import { render, screen } from '../test-utils';
import ThemedButton from './ThemedButton';
test('renders with theme context', () => {
render(Themed Button );
expect(screen.getByRole('button')).toHaveClass('btn-light'); // default theme
});
Testing Async Operations
// UserProfile.js
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
})
.then(userData => {
setUser(userData);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]);
if (loading) return Loading...;
if (error) return Error: {error};
return (
{user.name}
{user.email}
);
}
// UserProfile.test.js
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
// Mock fetch
global.fetch = jest.fn();
describe('UserProfile Component', () => {
beforeEach(() => {
fetch.mockClear();
});
test('displays user data after loading', async () => {
const mockUser = { name: 'John Doe', email: 'john@example.com' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
render( );
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
test('displays error message on fetch failure', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
render( );
await waitFor(() => {
expect(screen.getByText(/error: network error/i)).toBeInTheDocument();
});
});
});
Testing Custom Hooks
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
describe('useCounter Hook', () => {
test('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('initializes with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
Integration Testing
// App.test.js - Integration test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
test('complete user flow', async () => {
const user = userEvent.setup();
render( );
// Navigate to login
await user.click(screen.getByRole('link', { name: /login/i }));
// Fill login form
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: /login/i }));
// Verify dashboard is shown
expect(screen.getByText(/welcome to dashboard/i)).toBeInTheDocument();
});
Best Practices
- Test behavior, not implementation details
- Use semantic queries (getByRole, getByLabelText)
- Write tests that resemble how users interact with your app
- Mock external dependencies and APIs
- Test error states and edge cases
- Keep tests simple and focused
- Use descriptive test names
Practical Exercise
Create comprehensive tests for a Todo application including:
- Adding new todos
- Marking todos as complete
- Filtering todos by status
- Deleting todos
- Error handling