React Native Testing Library recommended practices for 2024
Published: 2024-01-26 23:03:50

React Native Testing Library has come a long way since I started contributing to it after joining Callstack in the pandemic year of 2020. Together with other contributors, we added many useful features over time, so some advice on the web and project practices might need to be updated. This post will describe some essential RNTL techniques you should use in 2024.

## 1. Use Screen object for queries

In the past, you had to capture the output from the `render()` function and use object destructuring to capture relevant queries:

```ts
// Old way
const { getByText, queryByText } = render(<UiToRender />);
const text = getByText(/Hello/i);
```

This required constant tweaking of render result destructuring, distracting you from writing your tests. Some people used a clever hack to avoid that hassle. Heck, we even have it in the RNTL test base as well.

```ts
// Old way 2: the clever hack
const view = render(<UiToRender />);
const text = view.getByText(/Hello/i);
```

That was somewhat better, but since 2022, you can use the [Screen API](https://callstack.github.io/react-native-testing-library/docs/api#screen-api).

```ts
// Modern way
render(<UiToRender />)
const text = screen.getByText(/Hello/i);
```

Just don’t forget to import it along with other utils from the RNTL package:

```ts
import { render, screen } from '@testing-library/react-native';
```

## 2. Use Integrated Jest matchers

In 2023, together with the RNTL team, we decided it would be a good idea to rewrite legacy Jest Native matches and integrate them into the RNTL codebase. The exact reasoning behind this decision deserves a short post on its own. From the user's perspective, these matches allow writing high-level queries in a way that will have long-term support from RNTL maintainers.

```ts
// Old way
expect(getByText("Start")).toBeTruthy(); // Or .not.toBeNull()
```

It’s worth noting that the `getBy*` queries are assertions on their own, checking that exactly one element matches the predicate, so `toBeTruthy()` is essentially a no-op assertion.

```ts
// Modern way:
expect(getByText("Start")).toBeOnTheScreen();\
```

The recommended assertion has two benefits. The obvious one is it's more eventing what you are trying to test. The second, less obvious, is that it actually does something useful; it checks that the element is still mounted in the element tree, allowing you to pass previously captured elements that exist as objects but may have been unmounted.

```ts
const button = getByRole('button');

// Lots of things happening in between

// This checks if the 'button' didn't get unmounted
expect(button).toBeOnTheScreen();
```

You can learn more about the [integrated Jest matchers in the RNTL documentation](https://callstack.github.io/react-native-testing-library/docs/jest-matchers).

## 3. Use User Event

For a long time, RNTL recommended Fire Event API for triggering events. It’s a pretty simple API: it will traverse the element tree up, starting with the passed element, and try to find a matching (and enabled) event handler. There are two variants of the API:

```ts
// Base variant
fireEvent(element, 'press', eventData...);

// Handy alias for: press, changeText, and scroll events
fireEvent.press(element, eventData...);
```

There are a couple of problems with Fire Event API:

1. It triggered only a single event, even when typical event interaction comprises multiple events (e.g., `pressIn`, `press`, `pressOut`).

2. It required you to explicitly specify the event object or otherwise passed `undefined` value.

3. For various reasons, it triggered events on both host and composite elements, which sometimes could result in unexpected behavior.

```ts
// Modern way
const user = userEvent.setup(); // Do once at the start of the test

await user.press(element);
```

There are few crucial differences:

1. User Event will send several events corresponding to React Native runtime behavior for a given interaction. For `press()`, that will be `pressIn`, `press` and `pressOut` events. However, for `type()`, there will be several events for **each letter** being typed, resulting in a much more realistic testing experience.

2. User Event will create event data objects for each passed event, which will have the same structure as the RN runtime ones.

3. User Event will only call events on host components, realistically simulating the RN runtime behavior.

4. User Event interactions are async functions, as there is a pause after each event, allowing JavaScript microtasks to resolve as they would in the RN runtime.

To remove any uncertainty, the Fire Event API is still supported. It will be kept for a long time, but we recommend using User Event when available for a given interaction, making your tests more realistic.

You can learn more about the [User Event API in the RNTL documentation](https://callstack.github.io/react-native-testing-library/docs/user-event).

## 4. Use semantic queries

Traditionally, the most popular queries were `getByText()` and `getByTestId()`. They are simple and easy to understand. Each of them has their own problems:

`getByText()` will find the given text in the rendered element tree, which will always be low-level host `Text` component. This causes a problem if you are looking, e.g., for a button with a given text and want to make some assertion on it.

```tsx
<Pressable onPress={...}>
  <Text>Next</Text> // <-- getByText() will return this
</Pressable>
```

`getByTestId()`, on the other hand allowed you to find exactly the component you wanted to find, but had nothing with what the user could observe on the screen.

```tsx
<Pressable testID="button-next" onPress={...}>
  <Text>Next</Text>
</Pressable>
```

The recommended approach nowadays is to use so called semantic queries, the most important of them being `getByRole()`:

```ts
// Modern way
const button = screen.getByRole("button", { name: "Next" });
```

This approach addresses the above pain points: it will return a button with the given user-visible text. At the same time, it will guide you in applying proper accessibility props to your components, which will also improve the UX of your app for users with assistive technology like screen readers.

Besides `getByRole()`, other semantic queries include:
* `getByLabelText()`
* `getByPlaceholderText()`
* `getByDisplayValue()`

You can learn more about the [recommended queries in the RNTL documentation](https://callstack.github.io/react-native-testing-library/docs/how-should-i-query).

## Summary

While there are many more relevant best practices, using the above recommendations will make your tests more solid and future-proof. Happy testing.