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.

Use our built-in contexts

By default, render() has some default contexts setup for you. The organization context is set to OrganizationFixture() and the router context has some default params like orgId and projectId. These can be overriden by passing a proptery to the 2nd argument of the render() function.

Example of overriding the default organization features and access:

Copied
import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen} from "sentry-test/reactTestingLibrary";

const organization = OrganizationFixture({access: ['org:admin'], features: ['my-feature-flag']});
// useOrganization will now get the above organization
render(<Example />, {organization});

Router context can be overriden in the same way.

Copied
const {organization, router, routerContext} = initializeOrg({
  organization: {features: ['global-views', 'open-membership']},
  router: {
    location: {
      pathname: '/organizations/org-slug/issues/',
      query: {environment: 'prod'},
    },
    params: {},
  },
});

render(<Example />, {context: routerContext, organization});
await userEvent.click(something);
expect(router.push).toHaveBeenCalledTimes(1);

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:

  1. 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 option getByRole('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.
  2. getByLabelText/getByPlaceholderText - Users find form elements using label text, therefore this option is prefered when testing forms.
  3. 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).
  4. 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.

Copied
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.

Copied
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.

Copied
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.

Copied
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.

Copied
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.

Copied
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.

Copied
// ❌
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");
You can edit this page on GitHub.