React Testing Library
React Testing Library (RTL) is a lightweight solution for testing React components. Its guiding principle is simple but profound:
"The more your tests resemble the way your software is used, the more confidence they can give you."
Instead of testing implementation details (like component state or lifecycle methods), RTL encourages you to test the output (the DOM) and user interactions.
Core Concepts
1. render and screen
The render function renders a React element into an isolated DOM node. You then use screen to query the DOM.
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message', () => {
render(<Greeting name="John" />);
// Querying the DOM
const heading = screen.getByRole('heading', { name: /hello john/i });
expect(heading).toBeInTheDocument();
});
2. Querying the DOM
RTL provides different types of queries. Knowing which one to use is crucial:
getBy...: Returns the matching element. Throws an error if no element or multiple elements are found. Use this by default.queryBy...: Returns the matching element ornullif not found. Useful for asserting that an element is not present.findBy...: Returns a Promise that resolves when an element is found. Useful for testing asynchronous updates.
Query Priorities
RTL emphasizes querying the DOM the way a user (or assistive technology) would. The recommended order of queries is:
getByRole: (e.g., button, heading, link) - best for accessibility!getByLabelText: Great for form fields.getByPlaceholderTextgetByText: Good for non-interactive elements.getByTestId: Use only as a last resort when semantic queries don't work.
3. User Interactions with userEvent
While RTL provides fireEvent, it's highly recommended to use @testing-library/user-event instead. userEvent simulates full user interactions (like typing, clicking) which trigger the exact sequence of DOM events a real user would cause.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Login from './Login';
test('allows the user to login', async () => {
const user = userEvent.setup();
render(<Login />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /log in/i });
// Simulate real user typing and clicking
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
expect(screen.getByText(/welcome back/i)).toBeInTheDocument();
});
4. Asynchronous Testing
When a component fetches data or updates state asynchronously, use findBy or waitFor.
test('loads and displays user data', async () => {
render(<UserProfile />);
// Initially, it might show a loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// findBy waits for the element to appear
const userName = await screen.findByRole('heading', { name: /jane doe/i });
expect(userName).toBeInTheDocument();
});
Summary
By using React Testing Library to query by roles and interact via userEvent, you implicitly verify that your application is accessible and behaves correctly from a user's perspective, making your tests highly resilient to refactoring.