What is React Testing Library and How it Works
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:
getByRole- Queries based on accessibility role (preferred)getByLabelText- Queries form elements by their labelgetByPlaceholderText- Queries by placeholder attributegetByText- Queries by visible text contentgetByTestId- 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 togetherdescribe("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 valuesexpect(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 checktoEqual(value)- Deep equality check for objectstoBeTruthy()- Checks if value is truthytoBeFalsy()- Checks if value is falsytoContain(item)- Checks if array contains itemtoThrow()- 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 functionconst mockFn = jest.fn();mockFn("hello");expect(mockFn).toHaveBeenCalledWith("hello");
// Mock a modulejest.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 documentexpect(element).toBeInTheDocument();
// Check visibilityexpect(element).toBeVisible();
// Check if element is disabledexpect(element).toBeDisabled();expect(element).toBeEnabled();
// Check if element has certain text contentexpect(element).toHaveTextContent("Hello");
// Check if element has certain attributeexpect(element).toHaveAttribute("href", "/about");
// Check if element has certain classexpect(element).toHaveClass("active");
// Check form valuesexpect(form).toHaveFormValues({ username: "testuser", rememberMe: true,});
// Check if checkbox is checkedexpect(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:
- The input gets focused
- Key down events fire
- Key press events fire
- The input value changes
- 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-eventconst user = userEvent.setup();
// Type in an input (simulates full keyboard interaction)await user.type(input, "Hello World");
// Click a buttonawait user.click(button);
// Select an optionawait user.selectOptions(select, "option1");
// Upload a fileawait 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 interactionsimport userEvent from '@testing-library/user-event';// jest-dom is typically imported in a setup file, extending expect automatically
// Import the component you're testingimport { 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
- Jest runs the test file and provides the
describe,test, andexpectfunctions - React Testing Library's
rendermounts your component into a virtual DOM - DOM Testing Library's
screenand queries let you find elements by accessible roles - user-event simulates realistic user interactions
- jest-dom matchers provide readable, DOM-specific assertions
- 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:
| Tool | What It Does |
|---|---|
| DOM Testing Library | Provides query methods to find DOM elements by accessible roles, text, labels, etc. |
| React Testing Library | Adds React-specific utilities like render on top of DOM Testing Library |
| Jest / Vitest | The test runner that executes tests and provides describe, test, expect, mocking, etc. |
| jest-dom | Extends Jest with DOM-specific matchers like toBeInTheDocument, toBeDisabled, etc. |
| user-event | Simulates 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.