React Testing Library (RTL) is a library that changes how we test React components. Instead of testing internal implementation, RTL focuses on what the user sees and does -- exactly like a mission control panel that shows the pilot only what matters.
"The more your tests resemble the way your software is used, the more confidence they can give you." -- Kent C. Dodds
The
render function is the foundation of RTL -- it fires up a component in a virtual DOM:1import { render, screen } from '@testing-library/react';
2import SpaceshipDashboard from './SpaceshipDashboard';
3
4test('renders spaceship dashboard', () => {
5 render(<SpaceshipDashboard pilotName="Nova" />);
6 // Component is now available for testing
7 expect(screen.getByText('Welcome, Nova!')).toBeInTheDocument();
8});RTL offers three query families, each for a different purpose:
1// Throws error if element does NOT exist
2const heading = screen.getByText('Mission Control');
3const input = screen.getByRole('textbox');
4const img = screen.getByAltText('spaceship');1// Returns null if element does NOT exist (no error)
2const error = screen.queryByText('Error occurred');
3expect(error).not.toBeInTheDocument();1// Waits for element to appear (returns Promise)
2const data = await screen.findByText('Data loaded');
3expect(data).toBeInTheDocument();Each query family has variants:
1// Searches by ARIA role -- the best approach!
2screen.getByRole('button', { name: 'Launch' });
3screen.getByRole('heading', { level: 1 });
4screen.getByRole('textbox', { name: /pilot name/i });
5screen.getByRole('checkbox', { checked: true });
6screen.getByRole('link', { name: 'Dashboard' });1screen.getByText('Mission Status: Active');
2screen.getByText(/mission/i); // regex, case-insensitive1// <label htmlFor="speed">Speed</label>
2// <input id="speed" />
3screen.getByLabelText('Speed');1screen.getByPlaceholderText('Enter coordinates');1// <div data-testid="fuel-gauge">...</div>
2screen.getByTestId('fuel-gauge');RTL recommends the following priority (from best):
The
@testing-library/jest-dom library adds special matchers:1// Visibility
2expect(element).toBeVisible();
3expect(element).toBeInTheDocument();
4
5// Attributes
6expect(button).toBeDisabled();
7expect(input).toBeRequired();
8expect(input).toHaveValue('Apollo');
9
10// CSS classes
11expect(element).toHaveClass('active');
12
13// Text
14expect(element).toHaveTextContent('Mission');
15
16// Styles
17expect(element).toHaveStyle({ color: 'green' });Components often need providers (Router, Theme, Store):
1function renderWithProviders(ui, options = {}) {
2 function Wrapper({ children }) {
3 return (
4 <ThemeProvider theme="dark">
5 <MissionProvider>
6 {children}
7 </MissionProvider>
8 </ThemeProvider>
9 );
10 }
11 return render(ui, { wrapper: Wrapper, ...options });
12}
13
14// Usage
15test('renders themed dashboard', () => {
16 renderWithProviders(<Dashboard />);
17 expect(screen.getByText('Dark Mode')).toBeInTheDocument();
18});When a test fails and you don't know why, use
screen.debug() to inspect the current DOM state:1test('shows mission status', () => {
2 render(<MissionPanel />);
3
4 // Displays current DOM state in the console
5 screen.debug();
6
7 // You can also debug a specific element
8 const panel = screen.getByRole('main');
9 screen.debug(panel);
10});screen.debug() will display the HTML of the entire rendered component, which helps understand why a selector can't find an element. It's like a diagnostic system on the spaceship -- when something doesn't work, you check the logs first!React Testing Library is the foundation of modern React component testing. Instead of testing implementation details, you test what really matters -- the user experience.