React Testing Library
Test React components the way users interact with them
๐งช What is React Testing Library?
React Testing Library is a testing tool that helps you test React components by simulating user interactions. It encourages testing behavior rather than implementation details, making tests more reliable and maintainable.
import { render, screen } from '@testing-library/react';
test('renders welcome message', () => {
render(<App />);
const element = screen.getByText(/welcome/i);
expect(element).toBeInTheDocument();
});
Result:
โ Test passes if "welcome" text is found in the component.
Core Testing Concepts
render()
Renders components for testing
const { container } = render(
<MyComponent />
);
screen
Queries rendered elements
const button = screen.getByRole(
'button', { name: /submit/i }
);
userEvent
Simulates user interactions
await userEvent.click(button);
await userEvent.type(input, 'text');
Assertions
Verifies expected behavior
expect(element)
.toBeInTheDocument();
๐น Basic Component Test
Start with a simple test that renders a component and checks if text appears. This is the foundation of React Testing Library.
// Button.jsx
import React from 'react';
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
export default Button;
// Button.test.jsx
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renders button with text', () => {
// Render the component
render(<Button>Click Me</Button>);
// Find the button
const button = screen.getByText('Click Me');
// Assert it exists
expect(button).toBeInTheDocument();
});
Output:
โ PASS: Button renders with correct text
๐น Testing User Interactions
Test how components respond to user actions like clicks, typing, and form submissions. Use userEvent for realistic user interactions.
// Counter.jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
export default Counter;
// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('increments count on button click', async () => {
// Setup user event
const user = userEvent.setup();
// Render component
render(<Counter />);
// Find elements
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText(/count: 0/i);
// Initial state
expect(count).toBeInTheDocument();
// Click button
await user.click(button);
// Check updated state
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
Output:
โ PASS: Counter increments from 0 to 1 on click
๐น Query Methods
React Testing Library provides different query methods to find elements. Choose the right query based on what you're testing and how users interact with your app.
Query Types:
- getBy... - Returns element or throws error (use for elements that should exist)
- queryBy... - Returns element or null (use to check if element doesn't exist)
- findBy... - Returns promise (use for async elements)
import { render, screen } from '@testing-library/react';
test('demonstrates query methods', async () => {
render(<MyComponent />);
// getByRole - Best for accessibility
const button = screen.getByRole('button', { name: /submit/i });
// getByText - Find by visible text
const heading = screen.getByText('Welcome');
// getByLabelText - For form inputs
const input = screen.getByLabelText('Email');
// getByPlaceholderText - By placeholder
const search = screen.getByPlaceholderText('Search...');
// queryByText - Returns null if not found
const missing = screen.queryByText('Not Here');
expect(missing).not.toBeInTheDocument();
// findByText - Wait for async element
const async = await screen.findByText('Loaded Data');
});
Output:
Use getByRole for best accessibility and maintainability.
๐น Testing Forms
Forms are common in React apps. Test form inputs, validation, and submission to ensure they work correctly for users.
// LoginForm.jsx
import React, { useState } from 'react';
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}
// LoginForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('submits form with email and password', async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
// Find form elements
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
// Fill out form
await user.type(emailInput, '[email protected]');
await user.type(passwordInput, 'password123');
// Submit form
await user.click(submitButton);
// Verify submission
expect(mockSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
});
});
Output:
โ PASS: Form submits with correct email and password values
๐น Testing Async Operations
Many components fetch data or perform async operations. Use findBy queries and waitFor to test async behavior properly.
// UserProfile.jsx
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'John Doe' })
})
);
test('displays user name after loading', async () => {
render(<UserProfile userId={1} />);
// Check loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for user name to appear
const userName = await screen.findByText('John Doe');
expect(userName).toBeInTheDocument();
// Loading should be gone
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
Output:
โ PASS: Component shows loading, then displays user name