Testing React Components (Intro)
Testing React components requires a different approach than testing plain functions. In this lesson, we'll cover the basics of component testing using Jest with React Testing Library.
Setup for React Testing
First, install the necessary packages:
npm install --save-dev @testing-library/react @testing-library/jest-dom
Configure Jest for React in jest.config.js:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy'
}
};
Your First Component Test
Let's test a simple component:
// Button.jsx
function Button({ onClick, children, disabled }) {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
);
}
export default Button;
// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
it('renders children correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByText('Click me')).toBeDisabled();
});
});
Queries: Finding Elements
React Testing Library provides several ways to find elements:
Priority Order (Best to Worst)
- getByRole - Accessible to everyone
- getByLabelText - Good for form fields
- getByPlaceholderText - For inputs
- getByText - For non-interactive elements
- getByTestId - Escape hatch (last resort)
function LoginForm() {
return (
<form>
<label htmlFor="email">Email</label>
<input id="email" type="email" placeholder="Enter email" />
<label htmlFor="password">Password</label>
<input id="password" type="password" />
<button type="submit">Sign In</button>
<span data-testid="error-message">Invalid credentials</span>
</form>
);
}
// Finding elements
screen.getByRole('button', { name: 'Sign In' }); // Best
screen.getByLabelText('Email'); // For form inputs
screen.getByPlaceholderText('Enter email'); // By placeholder
screen.getByText('Invalid credentials'); // By text content
screen.getByTestId('error-message'); // Last resort
Query Variants
| Prefix | Returns | Throws on No Match |
|---|---|---|
| getBy | Element | Yes |
| queryBy | Element or null | No |
| findBy | Promise | Yes (async) |
// getBy - throws if not found
screen.getByText('Hello'); // Error if missing
// queryBy - returns null if not found
const element = screen.queryByText('Maybe exists');
expect(element).toBeNull();
// findBy - waits for element (async)
const element = await screen.findByText('Loading complete');
Testing User Interactions
Use fireEvent or userEvent for interactions:
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// fireEvent - simple events
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'new value' } });
// userEvent - more realistic (recommended)
const user = userEvent.setup();
await user.click(button);
await user.type(input, 'new value');
await user.clear(input);
await user.selectOptions(select, 'option1');
Example: Testing a Form
// ContactForm.jsx
function ContactForm({ onSubmit }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
placeholder="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
// ContactForm.test.jsx
describe('ContactForm', () => {
it('submits form data', async () => {
const handleSubmit = jest.fn();
const user = userEvent.setup();
render(<ContactForm onSubmit={handleSubmit} />);
await user.type(screen.getByPlaceholderText('Name'), 'Alice');
await user.type(screen.getByPlaceholderText('Email'), 'alice@example.com');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Alice',
email: 'alice@example.com'
});
});
});
Testing Async Components
Components that fetch data need async testing:
// UserProfile.jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId)
.then(setUser)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>Hello, {user.name}!</div>;
}
// UserProfile.test.jsx
jest.mock('./api', () => ({
fetchUser: jest.fn()
}));
import { fetchUser } from './api';
describe('UserProfile', () => {
it('shows loading then user data', async () => {
fetchUser.mockResolvedValue({ name: 'Alice' });
render(<UserProfile userId={1} />);
// Loading state
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for data
await screen.findByText('Hello, Alice!');
// Loading gone
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
});
Testing Component State Changes
// Counter.jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>Count: {count}</span>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setCount(c => c - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
// Counter.test.jsx
describe('Counter', () => {
it('starts at zero', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
it('increments when clicking increment', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: 'Increment' }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('decrements when clicking decrement', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: 'Decrement' }));
expect(screen.getByText('Count: -1')).toBeInTheDocument();
});
it('resets to zero', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: 'Increment' }));
await user.click(screen.getByRole('button', { name: 'Increment' }));
await user.click(screen.getByRole('button', { name: 'Reset' }));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
});
Common Testing Library Matchers
From @testing-library/jest-dom:
// Presence
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
// Visibility
expect(element).toBeVisible();
expect(element).not.toBeVisible();
// Form state
expect(input).toBeDisabled();
expect(input).toBeEnabled();
expect(input).toBeRequired();
expect(input).toHaveValue('some value');
expect(checkbox).toBeChecked();
// Content
expect(element).toHaveTextContent('Hello');
expect(element).toBeEmptyDOMElement();
// Classes and styles
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });
// Attributes
expect(element).toHaveAttribute('href', '/home');
Try It: Test This Component
Loading Code Editor...
Key Takeaways
- Use React Testing Library for component tests
- Query elements by role, label, or text (avoid testId when possible)
- Use
userEventfor realistic user interactions - Use
findByqueries for async elements - Mock API calls and external dependencies
- Test behavior, not implementation details
- Test what users see and interact with
Next, we'll explore code coverage and how to measure your test effectiveness!

