Introduction to Testing page

Learn about testing solutions in React Native.

Overview

In this section, you will:

  • Learn about React Native Testing Library.
  • See the distinctions of testing for React Native components.
  • Use userEvent from @testing-library/react-native to simulate user interactions.

Objective 1: Testing in React Native

How do we know when our code is working correctly? How do we know it’s still working correctly in the future after we make changes to it?

Testing helps by verifying that given certain inputs our code generates expected outputs. So far we’ve copied existing tests to prove that we’ve completed the exercise correctly, now let’s dive in and learn about how React Native testing is done.

Introducing React Native Testing Library

React Native Testing Library (RNTL) is a set of tools that helps us write robust tests for our React Native applications. Unlike some other testing libraries that focus on the internal state and implementation details of components, RNTL emphasizes testing the user interface (UI) behavior as experienced by the end users.

In order to test components, it provides light utility functions using React Test Renderer. Most testing libraries use the DOM, but since React Native doesn’t use the DOM, the React Test Renderer is necessary.

Akin to the original React Testing Library, RNTL’s north star is:

The more your tests resemble the way your software is used, the more confidence they can give you.

RNTL encourages tests that focus on how the user interacts with the application, rather than the implementation details of the components.

Rendering and verifying a component in a test

OK, you’re convinced that RNTL is a good idea, how do you use it to create tests for your components?

Let’s take a look at an example. Say we want to test a component we created named EmailInputField.

import { useState } from "react"
import { SafeAreaView, ScrollView, Text, TextInput, View } from "react-native"

const EmailInputField: React.FC<Props> = ({ label, value }) => {
  const [formValue, setFormValue] = useState(value)

  return (
    <View>
      <Text nativeID="formLabel">{label}:</Text>
      <TextInput
        accessibilityLabel="input"
        accessibilityLabelledBy="formLabel"
        value={formValue}
        onChangeText={setFormValue}
      />
    </View>
  )
}

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ScrollView>
        <EmailInputField label="Email" value="test@example.com" />
      </ScrollView>
    </SafeAreaView>
  )
}

export default App

When rendered by React Native, this JSX will look something like:

<RCTSafeAreaView>
  <RCTScrollView>
    <View>
      <View>
        <Text nativeID="formLabel">Email :</Text>
        <TextInput
          accessibilityLabel="input"
          accessibilityLabelledBy="formLabel"
          value="test@example.com"
        />
      </View>
    </View>
  </RCTScrollView>
</RCTSafeAreaView>

We want to test EmailInputField. If you have experience with software testing, the following pattern will probably be recognizable: each test consists of arguments to the it function provided by Jest. The first argument is a short description of what the test is verifying. The convention is that the description string takes "it" as a prefix and proceeds from there, e.g. "[it] renders the correct label and value."

The second argument to it is a callback function that is called to run the test. Inside the callback, invoke the React Native Testing Library function render and pass it a single argument, the JSX for your component including props. After render completes, use screen to query the React Test Renderer and make assertions about the result.

import { render, screen } from "@testing-library/react-native"

import EmailInputField from "./EmailInputField"

it("renders the correct label and value", () => {
  render(<EmailInputField label="Email" value="test@example.com" />)
  const label = screen.getByText("Email:", { exact: false })
  expect(label).toBeOnTheScreen()
})

In the test above, we validate that the label is correct. We use the getByText function to select a single component whose text matches the string, "Email:". If you look closely, you can see that the <Text nativeID="formLabel"> content in the component has a ":" (colon) at the end, but the label prop does not, so we can conclude that EmailInputField appends the colon — but the purpose of the test isn’t how EmailInputField works, it’s the output that it returns.

In addition, we add {exact: false} as a second argument for getByText. This will ignore extra spaces created by the component from formatting and only match focus on matching the provided text.

After we get a component, we then use expect and toBeOnTheScreen to verify the component was rendered properly. Our test passes because the generated screen is what we expect the user will perceive.

We also want to validate the <TextInput> component; let’s update the test:

import { render, screen } from "@testing-library/react-native"

import EmailInputField from "./EmailInputField"

it("renders the correct label and value", () => {
  render(<EmailInputField label="Email" value="test@example.com" />)
  const label = screen.getByText("Email:", { exact: false })
  expect(label).toBeOnTheScreen()

  // Validate the input value.
  const input = screen.getByDisplayValue("test@example.com")
  expect(input).toBeOnTheScreen()
})

We’ve used a different query to select the <TextInput> component: getByDisplayValue. It returns an input component whose value matches the provided string. RNTL uses React Testing Libraries Core API which has a suite of "query" functions that select components based on characteristics like: role, label text, text, display value, and title. Our test continues to pass because the input’s value in the screen matches what we expect.

Objective 2: Write a test for handling user interactions

Most components can respond to events raised by user interaction, like tapping a button. How can we test code that responds to these interactions?

The userEvent API provided by RNTL allows your tests to interact with your component in a way that more closely matches what would happen with real user interactions, where a single user action can raise multiple events. For example, when a text input is typed into, it can emit focus and input events. The userEvent API also has some helpful functionality to prevent interactions that users would not realistically be able to do, such as not firing events from a component that’s hidden.

All of the methods provided by user-event return a Promise, so we want to prefix the test callback function with async and use await when a user-event method is called.

Using userEvent to simulate user interactions

Referring back to the EmailInputField component:

import { useState } from "react"
import { SafeAreaView, ScrollView, Text, TextInput, View } from "react-native"

const EmailInputField: React.FC<Props> = ({ label, value }) => {
  const [formValue, setFormValue] = useState(value)

  return (
    <View>
      <Text nativeID="formLabel">{label}:</Text>
      <TextInput
        accessibilityLabel="input"
        accessibilityLabelledBy="formLabel"
        value={formValue}
        onChangeText={setFormValue}
      />
    </View>
  )
}

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ScrollView>
        <EmailInputField label="Email" value="test@example.com" />
      </ScrollView>
    </SafeAreaView>
  )
}

export default App

We want to write a test to verify that what the user types is displayed as the <TextInput>’s value. The userEvent API includes a method named type that we can use to send a sequence of key events to the <TextInput> component. The type method provides a good simulation of what happens when a user is typing. Before we use type in a test, we need to initialize user events with the setup method. Let’s look at the test:

import { render, screen, userEvent } from "@testing-library/react-native"

import EmailInputField from "./EmailInputField"

it("renders the correct label and value", () => {
  render(<EmailInputField label="Email" value="test@example.com" />)
  const label = screen.getByText("Email:", { exact: false })
  expect(label).toBeOnTheScreen()

  // Validate the input value.
  const input = screen.getByDisplayValue("test@example.com")
  expect(input).toBeOnTheScreen()
})

it("updates when an email is entered", async () => {
  const user = userEvent.setup()

  render(<EmailInputField label="Email" value="" />)

  const input = screen.getByLabelText("Email:")
  expect(input).toBeOnTheScreen()
  expect(input).toHaveDisplayValue("")

  await user.type(input, "test@example.com")
  expect(input).toHaveDisplayValue("test@example.com")
})

There are some notable differences compared to the previous test:

  • The userEvent module is imported.
  • A user is created with the userEvent.setup() function.
  • The callback function argument of it is prefaced with async.
  • We need to await the type method to let it complete.

After type completes, we can make assertions about the current state of the input, including the current value, which should have been updated with the value of the single type argument.

As we continue on with future lessons, along with the future tests we use for verify exercises, we’ll explain how we and why we go about our testing approach.

Next steps

Next we will learn about building custom components to make our React Native application more modular.