---
title: "Using React Testing Library"
url: https://develop.sentry.dev/frontend/using-rtl/
---

# 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:

* [eslint-plugin-jest-dom](https://github.com/testing-library/eslint-plugin-jest-dom)
* [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library)

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](https://develop.sentry.dev/frontend/using-rtl.md#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 overridden by passing a property to the 2nd argument of the `render()` function.

Example of overriding the default organization features and access:

```tsx
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 });
```

### [Add extra providers](https://develop.sentry.dev/frontend/using-rtl.md#add-extra-providers)

If your component needs more context, pass an `additionalWrapper` to `render()` to compose extra providers inside our default test providers.

```tsx
import { render, screen } from "sentry-test/reactTestingLibrary";

function MyExtraProviders({ children }: { children: React.ReactNode }) {
  return (
    <SomeContext.Provider value={{}}>{children}</SomeContext.Provider>
  );
}

render(<Example />, { additionalWrapper: MyExtraProviders });

expect(screen.getByText(/example/i)).toBeInTheDocument();
```

## [Testing route changes](https://develop.sentry.dev/frontend/using-rtl.md#testing-route-changes)

When using `render()`, an in-memory router is used, which will react to navigations with `useNavigate()` or interactions with `Link` components. If your component relies on the URL, you can define the initial state in `initialRouterConfig`. You can access the current router state by referencing the returned `router` class, as well as navigate programmatically with `router.navigate()`.

```tsx
const { router } = render(<TestComponent />, {
  initialRouterConfig: {
    location: {
      pathname: "/foo/",
      query: { page: "1" },
    },
  },
});

// Uses passes in config to set initial location
expect(router.location.pathname).toBe("/foo");
expect(router.location.query.page).toBe("1");

// Clicking links goes to the correct location
await userEvent.click(screen.getByRole("link", { name: "Go to /bar/" }));

// Can check current route on the returned router
expect(router.location.pathname).toBe("/bar/");

// Can test manual route changes with router.navigate
router.navigate("/new/path/");
router.navigate(-1); // Simulates clicking the back button
```

### [Route Param Values](https://develop.sentry.dev/frontend/using-rtl.md#route-param-values)

If you need to test route param values (as in `useParams()`), the `route` will need to be provided in the config:

```tsx
function TestComponent() {
  const { id } = useParams();
  return <div>{id}</div>;
}

const { router } = render(<TestComponent />, {
  initialRouterConfig: {
    location: {
      pathname: "/foo/123/",
    },
    route: "/foo/:id/",
  },
});

expect(screen.getByText("123")).toBeInTheDocument();
```

### [Outlet Context](https://develop.sentry.dev/frontend/using-rtl.md#outlet-context)

If you need to test outlet param values (as in `useOutletContext()`), you can pass them inside the `initialRouterConfig` object.

```tsx
function TestComponent() {
  const { id } = useOutletContext();
  return <div>{id}</div>;
}

const { router } = render(<TestComponent />, {
  initialRouterConfig: {
    outletContext: { id: "123" },
  },
});

expect(screen.getByText("123")).toBeInTheDocument();
```

### [Nested Routes](https://develop.sentry.dev/frontend/using-rtl.md#nested-routes)

If directly setting outlet context isn't enough, instead you want to render some nested routes you can do that too!

Suppose routes.tsx defines some nested routes like:

```tsx
{
  path: 'settings/',
  component: SettingsWrapper,
  children: [
    {index: true, component: SettingsIndex},
    {path: ':projectId/', component: ProjectSettings},
  ]
}
```

We can configure the in-memory router to know about just the route tree that you need. Remember to render the wrapper component itself, and the router + route child components will render themselves.

```tsx
// settingsWrapper.tsx
interface OutletContext {
  name: string;
}

export function useCustomOutletContext() {
  return useOutletContext<OutletContext>();
}

export default function SettingsWrapper() {
  const context: OutletContext = {name: "Default"};
  return <Outlet context={context} />;
}

// settingsIndex.tsx
function SettingsIndex() {
  const {name} = useCustomOutletContext();
  return <div>Settings > {name}</div>;
}

// projectSettings.tsx
function ProjectSettings() {
  const {name} = useCustomOutletContext();
  const {projectId} = useParams();
  return <div>Settings > {name} > Project: {projectId}</div>;
}

// settingsIndex.spec.tsx
render(<SettingsWrapper />, {
  initialRouterConfig: {
    location: {
      pathname: '/settings/',
    },
    route: '/settings/',
    children: [
      {
        index: true,
        element: <SettingsIndex />,
      },
    ],
  },
});
expect(screen.getByText('Settings > Default')).toBeInTheDocument();

// projectSettings.spec.tsx
render(<SettingsWrapper />, {
  initialRouterConfig: {
    location: {
      pathname: '/settings/123/',
    },
    route: '/settings/:projectId',
    children: [
      {
        path: ':projectId/',
        element: <ProjectSettings />,
      },
    ],
  },
});
expect(screen.getByText('Settings > Default > Project: 123')).toBeInTheDocument();
```

## [Querying](https://develop.sentry.dev/frontend/using-rtl.md#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](https://testing-library.com/docs/dom-testing-library/api-accessibility/#logroles) feature or consult the [list of available roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles).
2. `getByLabelText`/`getByPlaceholderText` - Users find form elements using label text, therefore this option is preferred 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](https://testing-playground.com/) together with the `screen.logTestingPlaygroundURL()` and their [browser extension](https://chrome.google.com/webstore/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano).

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](https://testing-library.com/docs/queries/about/).

## [Tips](https://develop.sentry.dev/frontend/using-rtl.md#tips)

Avoid destructuring query functions from render method, use `screen` instead ([examples](https://github.com/getsentry/sentry/pull/29312)). 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.

```javascript
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](https://github.com/getsentry/sentry/pull/29517)). The `getBy...` and `findBy...` variants will throw a more helpful error message if no element is found.

```javascript
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](https://github.com/getsentry/sentry/pull/29544)). 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.

```javascript
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](https://github.com/getsentry/sentry/pull/29547)). The latter uses `MutationObserver` which is more efficient than polling the DOM at regular intervals with waitFor.

```javascript
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](https://github.com/getsentry/sentry/pull/29508)). The advantages of using these recommended assertions are better error messages, overall semantics, consistency, and uniformity.

```javascript
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.

```javascript
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.

```javascript
// ❌
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");
```

## [Testing hooks with providers](https://develop.sentry.dev/frontend/using-rtl.md#testing-hooks-with-providers)

Use `renderHookWithProviders()` to test hooks with the same built-in providers as `render()` (organization, theme, query client, and an in-memory router). It returns the regular `renderHook` result plus a `router` helper you can use to inspect location and navigate.

Set the initial URL and define route patterns via `initialRouterConfig` (use `route` for a single route or `routes` for multiple):

```tsx
import { useParams } from "react-router-dom";
import {
  renderHookWithProviders,
  waitFor,
} from "sentry-test/reactTestingLibrary";

function useRouteId() {
  const { id } = useParams();
  return id;
}

const { result, router } = renderHookWithProviders(useRouteId, {
  initialRouterConfig: {
    location: { pathname: "/foo/123/" },
    route: "/foo/:id/",
  },
});

expect(result.current).toBe("123");

// Navigate programmatically
router.navigate("/foo/456/");

await waitFor(() => expect(result.current).toBe("456"));
```

You can also provide additional providers or override the default organization via options:

```tsx
import { OrganizationFixture } from "sentry-fixture/organization";
import { renderHookWithProviders } from "sentry-test/reactTestingLibrary";

function useFeatureFlag() {
  // implementation under test
}

function MyExtraProviders({ children }: { children: React.ReactNode }) {
  return (
    <SomeContext.Provider value={{}}>{children}</SomeContext.Provider>
  );
}

const organization = OrganizationFixture({
  features: ["my-feature-flag"],
});

const { result } = renderHookWithProviders(useFeatureFlag, {
  organization,
  additionalWrapper: MyExtraProviders,
});
```
