Using React Testing Library
Our tests are written using React Testing Library. In this guide, you'll find tips to follow best practices and avoid common pitfalls.
We have two ESLint rules in place to help with this:
We strive to write tests in a way that closely resembles how our application is used.
Rather than dealing with instances of rendered components, we query the DOM in the same way the user would. We find form elements by their label text (just like a user would), we find links and buttons from their text (like a user would).
As a part of this goal, we avoid testing implementation details so refactors (changes to implementation but not functionality) don't break the tests.
We are generally in favor of Use Case Coverage over Code Coverage.
Querying
- use
getBy...
as much as possible - use
queryBy...
only when checking for non-existence - use
await findBy...
only when expecting an element to appear after a change to the DOM that might not happen immediately
To ensure that the tests resemble how users interact with our code we recommend the following priority for querying:
getByRole
- This should be the go-to selector for almost everything. As a nice bonus with this selector, we make sure that our app is accessible. It will most likely be used together with the name optiongetByRole('button', {name: /save/i})
. The name is usually the label of a form element or the text content of a button, or the value of the aria-label attribute. If unsure, use the logRoles feature or consult the list of available roles.getByLabelText
/getByPlaceholderText
- Users find form elements using label text, therefore this option is prefered when testing forms.getByText
- Outside of forms, text content is the main way users find elements. This method can be used to find non-interactive elements (like divs, spans, and paragraphs).getByTestId
- As this does not reflect how users interact with the app, it is only recommended for cases where you can't use any other selector
If you still have trouble deciding which query to use, check out testing-playground.com together with the screen.logTestingPlaygroundURL()
and their browser extension.
Do not forget, that you can drop screen.debug()
anywhere in your test to see the current DOM.
Read more about queries in the official docs.
Tips
Avoid destructuring query functions from render method, use screen
instead (examples).
You won't have to keep the render call destructure up-to-date as you add/remove the queries you need. You only need to type screen
and let your editor's autocomplete take care of the rest.
import { render, screen } from "sentry-test/reactTestingLibrary";
// ❌
const { getByRole } = render(<Example />);
const errorMessageNode = getByRole("alert");
// ✅
render(<Example />);
const errorMessageNode = screen.getByRole("alert");
Avoid using queryBy...
for anything except checking for non-existence (examples).
The getBy...
and findBy...
variants will throw a more helpful error message if no element is found.
import { render, screen } from "sentry-test/reactTestingLibrary";
// ❌
render(<Example />);
expect(screen.queryByRole("alert")).toBeInTheDocument();
// ✅
render(<Example />);
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
Avoid using waitFor
for waiting on appearance, use findBy...
instead (examples).
These two are basically equivalent (findBy...
even uses waitFor
under the hood), but the findBy...
is simpler and the error message we get will be better.
import {render, screen, waitFor} from "sentry-test/reactTestingLibrary";
// ❌
render(<Example />);
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
// ✅
render(<Example />);
expect(await screen.findByRole("alert")).toBeInTheDocument();
Avoid using waitFor
for waiting on disappearance, use waitForElementToBeRemoved
instead (examples).
The latter uses MutationObserver
which is more efficient than polling the DOM at regular intervals with waitFor.
import {
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from "sentry-test/reactTestingLibrary";
// ❌
render(<Example />);
await waitFor(() =>
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
);
// ✅
render(<Example />);
await waitForElementToBeRemoved(() => screen.getByRole("alert"));
Prefer using jest-dom assertions (examples). The advantages of using these recommended assertions are better error messages, overall semantics, consistency, and uniformity.
import {render, screen} from "sentry-test/reactTestingLibrary";
// ❌
render(<Example />);
expect(screen.getByRole("alert")).toBeTruthy();
expect(screen.getByRole("alert").textContent).toEqual("abc");
expect(screen.queryByRole("button")).toBeFalsy();
expect(screen.queryByRole("button")).toBeNull();
// ✅
render(<Example />);
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveTextContent("abc");
expect(screen.queryByRole("button")).not.toBeInTheDocument();
Prefer using case insensitive regex when searching by text. It will make the tests slightly more resilient to change.
import {render, screen} from "sentry-test/reactTestingLibrary";
// ❌
render(<Example />);
expect(screen.getByText("Hello World")).toBeInTheDocument();
// ✅
render(<Example />);
expect(screen.getByText(/hello world/i)).toBeInTheDocument();
Use userEvent
over fireEvent
where possible.
userEvent
comes from the package @testing-library/user-event
which is built on top of fireEvent
, but it provides several methods that resemble the user interactions more closely.
// ❌
import {render, screen, fireEvent} from "sentry-test/reactTestingLibrary";
render(<Example />);
fireEvent.change(screen.getByLabelText("Search by name"), {
target: { value: "sentry" },
});
// ✅
import {render, screen, userEvent} from "sentry-test/reactTestingLibrary";
render(<Example />);
userEvent.type(screen.getByLabelText("Search by name"), "sentry");