Skip to content
All essays
WebMarch 14, 202412 min

React Testing Library Complete Guide

Master testing React components with React Testing Library best practices and patterns

Ü
Ümit Uz
Mobile & Full Stack Developer

Testing React components doesn't have to be complicated. React Testing Library (RTL) provides a simple and complete set of utilities for testing React components in a way that encourages good testing practices. Let's master this essential tool.

Philosophy: Test User Behavior, Not Implementation Details

The core philosophy of RTL is testing from the user's perspective. Your tests should interact with your components the way users do—finding elements by their label text, role, or other attributes that users perceive.

What to Test

Test what users see and interact with: rendered output, user interactions like clicks and form submissions, and state changes reflected in the UI. Don't test implementation details like internal state, component methods, or lifecycle methods.

Setup and Installation

Install RTL and its peer dependencies along with jest-dom for custom matchers.

bash
npm install --save-dev @testing-library/react @testing-library/jest-dom

Configure your test setup to include jest-dom matchers.

Basic Queries

RTL provides several queries for finding elements in the DOM. The most important ones mirror how users find elements.

Priority Order for Queries

  1. 1getByRole: For elements with ARIA roles (buttons, links, form elements)
  2. 2getByLabelText: For form inputs with associated labels
  3. 3getByPlaceholderText: For form inputs with placeholder text
  4. 4getByText: For non-interactive elements like headings and paragraphs
  5. 5getByDisplayValue: For form inputs with current values
  6. 6getByAltText: For images and graphics with alt text
  7. 7getByTitle: For elements with title attributes

getByRole Examples

getByRole is often the best query because it's accessible and mirrors how screen reader users find elements.

typescript
import { render, screen } from '@testing-library/react';

test('finds button by role', () => {
  render(<button>Submit</button>);
  const button = screen.getByRole('button', { name: 'Submit' });
  expect(button).toBeInTheDocument();
});

Testing User Interactions

Use fireEvent or userEvent to simulate user interactions. userEvent is preferred as it more closely simulates real browser behavior.

Click Events

typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('calls onClick when button is clicked', async () => {
  const handleClick = jest.fn();
  render(<button onClick={handleClick}>Click me</button>);

  await userEvent.click(screen.getByRole('button'));

  expect(handleClick).toHaveBeenCalledTimes(1);
});

Form Interactions

typescript
test('submits form with input values', async () => {
  const handleSubmit = jest.fn();
  render(<form onSubmit={handleSubmit}>
    <input name="username" />
    <button type="submit">Submit</button>
  </form>);

  await userEvent.type(
    screen.getByRole('textbox', { name: /username/i }),
    'john_doe'
  );
  await userEvent.click(screen.getByRole('button', { name: /submit/i }));

  expect(handleSubmit).toHaveBeenCalledWith(
    expect.objectContaining({
      username: 'john_doe'
    })
  );
});

Async Testing

Modern React applications are full of async operations—fetching data, updating state, and more. RTL provides utilities for testing async behavior.

findBy Queries for Async Elements

findBy queries wait for elements to appear, returning a promise that resolves when the element is found.

typescript
test('displays loading message then data', async () => {
  render(<UserProfile userId="1" />);

  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // waitForElement to be removed or findBy for element to appear
  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});

waitFor for Custom Conditions

waitFor lets you wait for any condition to be true, retrying until it passes or times out.

typescript
import { waitFor } from '@testing-library/react';

test('waits for callback to pass', async () => {
  render(<MyComponent />);

  await waitFor(() => {
    expect(screen.getByText('Success')).toBeInTheDocument();
  });
});

waitForElementToBeRemoved

Use this utility when waiting for loading states or modal overlays to disappear.

typescript
test('removes loading spinner after data loads', async () => {
  render(<DataLoader />);

  const spinner = screen.getByRole('status', { name: /loading/i });
  await waitForElementToBeRemoved(spinner);

  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

Testing Forms

Forms are central to most applications. Here's how to test them comprehensively.

Controlled Inputs

typescript
test('controlled input updates value', async () => {
  const handleChange = jest.fn();
  render(<input value="" onChange={e => handleChange(e.target.value)} />);

  const input = screen.getByRole('textbox');
  await userEvent.type(input, 'hello');

  expect(handleChange).toHaveBeenLastCalledWith('hello');
});

Form Validation

typescript
test('shows error message for invalid email', async () => {
  render(<ContactForm />);

  const emailInput = screen.getByLabelText(/email/i);
  const submitButton = screen.getByRole('button', { name: /submit/i });

  await userEvent.type(emailInput, 'invalid-email');
  await userEvent.click(submitButton);

  expect(screen.getByText(/please enter a valid email/i)).toBeInTheDocument();
});

Mocking and Stubbing

Sometimes you need to mock dependencies like API calls or external libraries.

Mocking API Calls

typescript
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json({ name: 'John Doe' }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('displays user data', async () => {
  render(<UserProfile />);

  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});

Mocking Child Components

typescript
jest.mock('./ChildComponent', () => ({
  ChildComponent: () => <div>Mocked Child</div>
}));

test('renders parent with mocked child', () => {
  render(<Parent />);
  expect(screen.getByText('Mocked Child')).toBeInTheDocument();
});

Testing Hooks

Use @testing-library/react-hooks to test custom hooks in isolation.

typescript
import { renderHook, act } from '@testing-library/react-hooks';

test('useCounter increments and decrements', () => {
  const { result } = renderHook(() => useCounter());

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Testing Context

Test components that consume context by wrapping them in the appropriate provider.

typescript
test('displays user name from context', () => {
  const mockUser = { name: 'John Doe' };
  render(
    <UserContext.Provider value={{ user: mockUser }}>
      <UserProfile />
    </UserContext.Provider>
  );

  expect(screen.getByText('John Doe')).toBeInTheDocument();
});

Testing Routing

For routing, you can use MemoryRouter from react-router-dom to test routes without needing browser navigation.

typescript
import { MemoryRouter } from 'react-router-dom';

test('navigates to about page', async () => {
  render(
    <MemoryRouter>
      <App />
    </MemoryRouter>
  );

  await userEvent.click(screen.getByText(/about/i));

  expect(screen.getByText(/about us/i)).toBeInTheDocument();
});

Accessibility Testing

RTL encourages accessible testing practices by default. Use jest-axe to check for accessibility issues.

typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('has no accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Best Practices

  1. 1Test user behavior, not implementation details
  2. 2Use getByRole as your primary query
  3. 3Avoid querying by test IDs unless necessary
  4. 4Test async behavior with findBy queries or waitFor
  5. 5Keep tests simple and focused
  6. 6Mock external dependencies appropriately
  7. 7Use descriptive test names that explain what is being tested
  8. 8One assertion per test when possible
  9. 9Arrange-Act-Assert pattern for clarity

Common Pitfalls

Avoid these testing mistakes: testing internal state instead of DOM output, using test IDs for everything instead of accessible queries, testing library implementation details, not cleaning up after tests, over-mocking instead of testing integration points, and testing too many things in a single test.

Conclusion

React Testing Library provides simple utilities that encourage testing from the user's perspective. By focusing on what users see and interact with, you write tests that are more resilient to implementation changes and better catch real bugs.

Master the queries, practice testing user interactions, handle async operations properly, and follow the philosophy of testing behavior over implementation. Your tests will be more valuable, maintainable, and will give you confidence that your application works as intended.

Remember: the more your tests resemble the way your software is used, the more confidence they can give you.

Enjoyed this essay? Share it.

Related essays

Next essay
Rust Borrowing and References