What is React Testing Library and How it Works

Avatar of Hemanta Sundaray

Hemanta Sundaray

Published

If you're getting started with testing React applications, you've probably encountered terms like "React Testing Library," "DOM Testing Library," "Jest," and "jest-dom." It can be confusing to understand what each of these tools does and how they fit together.

In this post, I'll break down the entire ecosystem so you understand exactly what React Testing Library is, what it's made of, and how all the pieces work together.

The Core Philosophy: Testing Like a User

Before diving into the tools, let's understand the philosophy that drives React Testing Library. The library was designed around one central idea: your tests should interact with your application the same way your users do.

Think about it this way. When a user visits your application, they don't care about your component's internal state. They don't know or care that you have a useState hook with a variable called isModalOpen. What they care about is: "Can I see the button? Can I click it? Does the modal appear?"

React Testing Library encourages you to write tests that answer these user-centric questions rather than testing implementation details like state values or method names.

The Building Blocks

React Testing Library is not a single monolithic tool. It's built on top of several smaller libraries that work together.

Let's explore each one.

1. DOM Testing Library: The Foundation

The DOM Testing Library is the foundation of everything. It provides a set of utilities for querying and interacting with DOM nodes.

What does it actually do?

The DOM Testing Library gives you methods to find elements in the DOM the same way users would find them. Instead of selecting elements by their CSS class names or test IDs (which users can't see), it encourages you to select elements by:

  • Their visible text
  • Their accessible role (more on this below)
  • Their label text
  • Their placeholder text

Here's a simple example. Say you have this HTML:

<button>Submit</button>

With DOM Testing Library, you can find this button using:

screen.getByRole("button", { name: /submit/i });

This query finds an element with the role of "button" that has the accessible name "Submit." This is exactly how a user (or a screen reader) would identify this element.

The screen Object

The screen object is your window into the DOM. It provides all the query methods you need to find elements. Think of it as representing what's currently rendered on the screen.

2. What About Accessibility and Screen Readers?

This is where the DOM Testing Library's philosophy really shines, and it's worth explaining in detail since the concept can be confusing at first.

What are Screen Readers?

Screen readers are software applications that help people with visual impairments use computers. They read out the content of the screen and allow users to navigate using keyboard commands.

What are ARIA Roles?

ARIA (Accessible Rich Internet Applications) is a set of attributes that define ways to make web content more accessible. Every HTML element has an implicit "role" that tells assistive technologies what kind of element it is:

  • A <button> has the role of "button"
  • An <input type="text"> has the role of "textbox"
  • An <a> (link) has the role of "link"
  • A <nav> has the role of "navigation"

When a screen reader encounters an element, it announces this role to the user. For example, it might say "Submit, button" when it reaches a submit button.

Why Does This Matter for Testing?

Here's the key insight: if you write tests that query elements by their accessible roles and names, you're simultaneously verifying that your application is accessible. If your test can find the button using getByRole('button', { name: /submit/i }), then a screen reader user can find that button too.

Consider this example:

<!-- Bad: A div styled to look like a button -->
<div class="btn" onclick="handleClick()">Submit</div>
<!-- Good: An actual button element -->
<button onclick="handleClick()">Submit</button>

If you try to use getByRole('button', { name: /submit/i }) on the first example, it will fail because a <div> doesn't have the role of "button." This failing test tells you that your "button" isn't actually accessible to screen reader users. The DOM Testing Library's query methods essentially enforce accessibility best practices.

The Query Priority

DOM Testing Library recommends using queries in this order of priority:

  1. getByRole - Queries based on accessibility role (preferred)
  2. getByLabelText - Queries form elements by their label
  3. getByPlaceholderText - Queries by placeholder attribute
  4. getByText - Queries by visible text content
  5. getByTestId - Queries by data-testid attribute (last resort)

The reason getByRole is preferred is because it most closely mirrors how assistive technologies interact with your page.

3. React Testing Library: The React Wrapper

React Testing Library is essentially the React-specific version of DOM Testing Library. It adds React-specific utilities on top of the core DOM Testing Library.

What Does It Add?

The main addition is the render function. Without React Testing Library, if you wanted to test a React component, you'd have to manually set up the DOM like this:

const div = document.createElement('div');
ReactDOM.render(<MyComponent />, div);
document.body.appendChild(div);

With React Testing Library, you simply write:

import { render, screen } from '@testing-library/react';
render(<MyComponent />);

The render function handles all the boilerplate of mounting your component into the DOM. After calling render, you can use the screen object (which comes from DOM Testing Library) to query for elements.

In addition to render, React Testing Library also provides other React-specific utilities.

4. Jest: The Test Runner

Now we need to talk about Jest (or Vitest, which is a newer, faster alternative). Jest is a test runner and testing framework.

What is a Test Runner?

A test runner is the program that actually executes your tests and reports the results. When you run npm test, Jest (or Vitest) is what finds your test files, runs them, and tells you whether they passed or failed.

What Jest Provides

Jest gives you the fundamental building blocks for writing tests:

// describe - Groups related tests together
describe("MyComponent", () => {
// test or it - Defines an individual test case
test("renders a greeting", () => {
// Your test code here
});
// it is an alias for test
it("handles click events", () => {
// Your test code here
});
});
// expect - Makes assertions about values
expect(someValue).toBe(expectedValue);
expect(someArray).toContain(item);
expect(someObject).toEqual({ key: "value" });

Built-in Matchers

Jest comes with many "matchers": methods that check if a value meets certain conditions:

  • toBe(value) - Strict equality check
  • toEqual(value) - Deep equality check for objects
  • toBeTruthy() - Checks if value is truthy
  • toBeFalsy() - Checks if value is falsy
  • toContain(item) - Checks if array contains item
  • toThrow() - Checks if function throws an error

Mocking

Jest also provides powerful mocking capabilities. You can mock functions, modules, timers, and more. This is essential for isolating the code you're testing from external dependencies.

// Mock a function
const mockFn = jest.fn();
mockFn("hello");
expect(mockFn).toHaveBeenCalledWith("hello");
// Mock a module
jest.mock("./api", () => ({
fetchUser: jest.fn(() => Promise.resolve({ name: "Test User" })),
}));

5. jest-dom: Enhanced DOM Assertions

Here's where many people get confused. Jest comes with general-purpose matchers like toBe and toEqual, but these aren't specifically designed for testing DOM elements. This is where jest-dom comes in.

What is jest-dom?

jest-dom is a companion library that adds DOM-specific matchers to Jest. It's designed to work perfectly with DOM Testing Library and React Testing Library.

Why Do You Need It?

Without jest-dom, if you want to check whether a button is disabled, you might write:

const button = screen.getByRole("button");
expect(button.hasAttribute("disabled")).toBe(true);

This works, but it's not very readable. With jest-dom, you can write:

const button = screen.getByRole("button");
expect(button).toBeDisabled();

Much clearer! The test reads almost like plain English: "expect button to be disabled."

Popular jest-dom Matchers

jest-dom provides many helpful matchers:

// Check if element is in the document
expect(element).toBeInTheDocument();
// Check visibility
expect(element).toBeVisible();
// Check if element is disabled
expect(element).toBeDisabled();
expect(element).toBeEnabled();
// Check if element has certain text content
expect(element).toHaveTextContent("Hello");
// Check if element has certain attribute
expect(element).toHaveAttribute("href", "/about");
// Check if element has certain class
expect(element).toHaveClass("active");
// Check form values
expect(form).toHaveFormValues({
username: "testuser",
rememberMe: true,
});
// Check if checkbox is checked
expect(checkbox).toBeChecked();

Better Error Messages

Another advantage of jest-dom is that it provides better error messages. If a test fails, jest-dom gives you context-specific feedback. For example:

Expected element to be disabled:
<button>Submit</button>
Received element that is not disabled.

This is much more helpful than a generic "expected true but received false" message.

6. user-event: Simulating User Interactions

There's one more piece of the puzzle worth mentioning: @testing-library/user-event. While the core DOM Testing Library includes fireEvent for triggering DOM events, user-event provides a more realistic simulation of user interactions.

fireEvent vs user-event

fireEvent dispatches DOM events directly, but real user interactions involve multiple events. For example, when a user types in an input:

  1. The input gets focused
  2. Key down events fire
  3. Key press events fire
  4. The input value changes
  5. Key up events fire

fireEvent.change only triggers the change event, skipping all the others. user-event simulates the complete interaction as a real user would perform it.

import userEvent from "@testing-library/user-event";
// Set up user-event
const user = userEvent.setup();
// Type in an input (simulates full keyboard interaction)
await user.type(input, "Hello World");
// Click a button
await user.click(button);
// Select an option
await user.selectOptions(select, "option1");
// Upload a file
await user.upload(fileInput, file);

Note that user-event methods are async, so you need to await them.

How Everything Works Together

Now let's see how all these pieces fit together in a complete test:

// Import from React Testing Library (which re-exports DOM Testing Library utilities)
import { render, screen } from '@testing-library/react';
// Import user-event for realistic user interactions
import userEvent from '@testing-library/user-event';
// jest-dom is typically imported in a setup file, extending expect automatically
// Import the component you're testing
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
test('submits the form with entered credentials', async () => {
// Arrange: Set up user-event and render the component
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Act: Interact with the form like a user would
// Find elements by their accessible roles and labels
const usernameInput = screen.getByRole('textbox', { name: /username/i });
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
// Type in the inputs
await user.type(usernameInput, 'testuser');
await user.type(passwordInput, 'password123');
// Click the submit button
await user.click(submitButton);
// Assert: Verify the expected behavior
// Using jest-dom matchers for clear, readable assertions
expect(handleSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123'
});
});
test('displays error message for invalid credentials', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={() => {}} error="Invalid credentials" />);
// jest-dom matcher to check if element is in the document
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
// Check that it has the error styling
expect(screen.getByRole('alert')).toHaveClass('error-message');
});
});

The Flow

  1. Jest runs the test file and provides the describe, test, and expect functions
  2. React Testing Library's render mounts your component into a virtual DOM
  3. DOM Testing Library's screen and queries let you find elements by accessible roles
  4. user-event simulates realistic user interactions
  5. jest-dom matchers provide readable, DOM-specific assertions
  6. Jest reports whether the test passed or failed

The Complete Picture

Here's a visual summary of how all the pieces fit together:

┌─────────────────────────────────────────────────────────────────┐
│ Jest (or Vitest) │
│ The Test Runner │
│ Provides: describe, test, it, expect, mock functions, etc. │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ jest-dom │ │
│ │ DOM-Specific Matchers │ │
│ │ toBeInTheDocument, toBeDisabled, toHaveTextContent, etc. │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ React Testing Library │ │
│ │ React-Specific Utilities │ │
│ │ render, cleanup │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ DOM Testing Library │ │ │
│ │ │ Core Query Methods │ │ │
│ │ │ screen, getByRole, getByText, getByLabelText, etc. │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ user-event │ │
│ │ Realistic User Interaction Simulation │ │
│ │ type, click, selectOptions, etc. │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Summary

Let's recap what each tool does:

ToolWhat It Does
DOM Testing LibraryProvides query methods to find DOM elements by accessible roles, text, labels, etc.
React Testing LibraryAdds React-specific utilities like render on top of DOM Testing Library
Jest / VitestThe test runner that executes tests and provides describe, test, expect, mocking, etc.
jest-domExtends Jest with DOM-specific matchers like toBeInTheDocument, toBeDisabled, etc.
user-eventSimulates realistic user interactions (typing, clicking, etc.)

The beauty of this ecosystem is that each tool does one thing well, and they're designed to work together seamlessly. When you import from @testing-library/react, you get both React Testing Library utilities and the underlying DOM Testing Library queries. When you set up jest-dom in your test configuration, all those extra matchers are automatically available on expect.

The core philosophy ties everything together: test your components the way users interact with them. Find elements by their accessible roles. Simulate realistic user behavior. Assert on what the user would see and experience. This approach leads to tests that are more maintainable, less likely to break during refactoring, and actually verify that your application works for all users, including those using assistive technologies.

TAGS:

Testing
What is React Testing Library and How it Works