Intro to React Native Testing Library (RNTL) Part I (useState and TextInput)

Rosen Toshev
7 min readApr 21, 2022

--

“the more your tests resemble the way your software is used, the more confidence they can give you” — Kent C. Dodds

Unit and integration testing has been a well-established concept of front-end development in React. In the early stages, Enzyme initially developed by Airbnb and later transferred to the enzymejs GitHub organization has been the preferred method to perform unit tests. Currently, JEST with the React Testing Library (RTL) seems like a logical choice as tests appear to have mostly transitioned to how the user would actually use the app.

Think about what happens when a button is triggered or when an InputText field gets populated. What changes in the UI for the user? Can we test exactly these changes? While there is plenty of courses and documentation on how to use the React Testing Library, I have not found a lot of materials on how to use the React Native Testing Library (RNTL). Therefore, during the course of several posts, I would like to cover the basics for anyone who may find it helpful.

Initial Setup

Out first step is to install the app. From react native 0.38 onwards, JEST setup comes by difficult when running the npx react-native init command.

npx react-native init RNTestLibraryApp
Installing CocoaPods error

Note: If you are on an M1 Mac, you may be getting the error on the screenshot above: Error: Failed to install CocoaPods dependencies for iOS project, which is required by this template. As one of the solutions, cd in the project folder and execute the following command in your terminal.

cd RNTestLibraryApp 
bundle install

Open the project folder in your VS Studio Code. Open the App.js and delete all of its content. We will start with the following code that you can directly paste.

import { View, Text } from 'react-native'import React from 'react'const App = () => {return (<View><Text>App</Text></View>)}export default App

Setting Up the App component

Now the next goal after pasting the bare App component is to create a screen with two TextInput fields, two state variables (ursename and userage), and two functions to be called when the username and userage are changed. This is the reason that we would need a TextInput component from react-native. After importing TextInput we can start setting up the component. Reading the official react native documentation, we see that onChangeText is a callback that is called when the TextInput’s value changes. We can set the changes in the state using the onChangeText prop.

const onChangeUserName = name => {setUsername(name);};<TextInputstyle={styles.input}onChangeText={text => onChangeUserName(text)}value={username}placeholder="username"/>

The completed App.js component should look like this:

import {SafeAreaView, View, Text, StyleSheet, TextInput} from 'react-native';import React, {useState} from 'react';const App = () => {const [username, setUsername] = useState('');const [userage, setUserage] = useState('');const onChangeUserName = name => {setUsername(name);};const onChangeUserAge = age => {setUserage(age);};return (<SafeAreaView><View style={styles.wrapper}><Text>Your new username is </Text><Text>{username}</Text><TextInputstyle={styles.input}onChangeText={text => onChangeUserName(text)}value={username}placeholder="Username"/><Text>Your new user age is </Text><Text>{userage}</Text><TextInputstyle={styles.input}onChangeText={text => onChangeUserAge(text)}value={userage}placeholder="Userage"/></View></SafeAreaView>);};
export default App;
const styles = StyleSheet.create({input: {height: 40,margin: 12,borderWidth: 1,padding: 10,},inputContainer: {justifyContent: 'flex-start',flexGrow: 1,},});

To make sure that these changes work run the following commands.

For an iOS device or a simulator

npx react-native run-ios 

For an Android device or a simulator

npx react-native run-android

Writing Our First Test

So far so good but something is missing. We have not yet implemented any actual tests. Furthermore, we haven’t even installed the library. In the root folder of our project execute the following commands:

yarn add --dev @testing-library/react-native
yarn add --dev @testing-library/jest-native

The official testing library documentation, states that the testIdattribute is used by the getByTestId()querySelector. We will put the attribute on our TextInput component. The convention that I follow, though certainly not a standard, is screenName.nameOfValue. In our case, the testId attributes of the two TextInput components would be App.username and App.userage. Change App.js accordingly.

<TextInputstyle={styles.input}onChangeText={text => onChangeUserName(text)}value={username}placeholder="Username"testID="App.username"/>...<TextInputstyle={styles.input}onChangeText={text => onChangeUserAge(text)}value={userage}placeholder="Userage"testID="App.userage"/>

On the root level of the project, we create a __tests__ folder together with a App.test.js file. The JEST API searches for files under the __tests__ folder. A good practice here is to name the test files with the same name as the file name of the component or screen we would like to test. The test file name for App.js would be App.test.js.

Project File Structure

The file structure for the project should look like this now. We would like to create a matching snapshot of our component for the tests. The toJSONprop provides us with the rendered component JSON representation for snapshot testing. We are ready to write our first test in the App.test.js file.

import React from 'react';import 'react-native';import App from '../App';import {render} from '@testing-library/react-native';test('renders correctly', () => {  const {toJSON} = render(<App />);  expect(toJSON()).toMatchSnapshot();});

We will be checking whether the test passes by running yarn test or npm run test. In our case, our single test in the App.test.js file should now pass. Let’s celebrate this by putting our repo on GitHub (link at the end). I will make a new branch for every milestone we reach in the app. The current one is called first-test. It is a good practice to automatically rerun jest when a file changes. In order to do this, we need to update thetest script in our package.json file in the following manner.

Updated the test script in our package.json file to automatically rerun jest

Remember the quote at the beginning of the post that our tests should resemble how our app is used. When we set a piece of data in the state, we would likely use it in the same component or pass it down as a prop to a child component. For the subsequent tests, we would be checking when triggering an event on the TextInput would change the value in the Text component displaying the state. Therefore, we are testing the changes of the values in the relevant UI element that display the state values.

The describe block is a JEST global that groups together several tests. The tests that fall within the scope of a describe block are usually related such as testing the same value. Here we will group together text input field data changes. Since describe is a JEST global, we do not need to import it.

It is now time to write these tests. But how do we know when an InputText field is populated? In the React Native Testing Library documentation, we see that the fireEvent.changeText invokes changeText event handler on the element or parent element in the tree. This sounds exactly like what we need. In order to find the Text element on the page, we need to use one of the query methods (get, find, query). The getByText method takes a string or a regular expression and finds any visible text on interactive and non-interactive elements. This is exactly what we need to test whether the username and userage are displayed as Text. We use toBeTruthy to ensure that a value is on the component in a boolean context.

This is our test for the username TextInput change.

test('Displays a username, if the username field has been completed', () => {const INPUT_TEXT = 'John';const {getByTestId, getByText} = render(component);const userNameTextInput = getByTestId('App.username');fireEvent.changeText(userNameTextInput, INPUT_TEXT);const username = getByText(INPUT_TEXT);expect(username).toBeTruthy();});

Similarly, this is our test for the userage TextInput change.

test('Displays a userage, if the userage field has been completed', () => {const INPUT_TEXT = '25';const {getByTestId, getByText} = render(component);const userAgeTextInput = getByTestId('App.userage');fireEvent.changeText(userAgeTextInput, INPUT_TEXT);const userage = getByText(INPUT_TEXT);expect(userage).toBeTruthy();});

Our entire App.test.js file should look like this.

import React from 'react';import 'react-native';import App from '../App';import {render, fireEvent} from '@testing-library/react-native';test('renders correctly', () => {  const {toJSON} = render(<App />);  expect(toJSON()).toMatchSnapshot();});
const component = <App />;
describe('Check input field data', () => { test('Displays a username, if the username field has been completed', () => { const INPUT_TEXT = 'John'; const {getByTestId, getByText} = render(component); const userNameTextInput = getByTestId('App.username'); fireEvent.changeText(userNameTextInput, INPUT_TEXT); const username = getByText(INPUT_TEXT); expect(username).toBeTruthy();});test('Displays a userage, if the userage field has been completed', () => { const INPUT_TEXT = '25'; const {getByTestId, getByText} = render(component); const userAgeTextInput = getByTestId('App.userage'); fireEvent.changeText(userAgeTextInput, INPUT_TEXT); const userage = getByText(INPUT_TEXT); expect(userage).toBeTruthy();});});

We run our tests again using yarn test or npm run test and we should now see that they all pass. We have reached the final milestone in part I of this tutorial. It is time to commit and push to a new branch called input-field-tests. In the following parts, we will add Redux, submit the data of the form by using a Pressable in our component, fetch and post data, and write the relevant tests.

All tests in App.test.js passing successfully

GitHub Repo: https://github.com/rosen777/RNTestLibraryApp

References

--

--

Rosen Toshev
Rosen Toshev

No responses yet