Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with mobile machine learning, React, React Native, and UI designing.

User behavior testing with React Native Testing Library

5 min read 1514

User Behavior Testing with React Native Testing Library

In frontend development, testing user behavior is crucial for delivering great products. React Native Testing Library (RNTL) makes it a joy to test user journeys in our React Native apps. It has great APIs to make tests immune to implementation details.

By the end of this tutorial, I promise you will feel this joy, too — and I won’t take up too much of your time getting there. You can find the full code for this demo on GitHub.

Getting started with React Native Testing Library

Let’s begin with adding libraries that we will be using to make our testing a joyful experience.

yarn add -D @testing-library/react-native

One thing to note here — RNTL is a wrapper over react-testing-library that depends on Jest. This combination, in my humble opinion, is a match made in heaven and makes the testing experience amazing.

Testing a Button component

Let’s start small; if you are familiar with testing components, you can skip this section. We will test a simple button component just to make ourselves a bit more familiar with how RNTL works and what we are up to. Here is how our Button component looks:

Code for the Button Component

Here’s the full code for the Button component. We will test that it should call the onPress method when it is pressed by a user. Our test should look like this:

Code for the Button Component Test Suite

RNTL provides a great API to grab components using attributes. Though there are a number of ways to grab a component, I feel getByTestId makes your test code more immune to UI and textual changes.

With the fireEvent API, we can mock many interactions, which allows us to test user behaviors. Here, we have created a mock method and asserted that it should be called only once when a user presses the button. We will use a similar structure for testing our flow further.

We made a custom demo for .
No really. Click here to check it out.

Here is how we will execute this test, along with the expected output:

Expected Output for Our Button Component Test

Cool, so we have done some basic testing now and have a good idea of how things will shape up.

Testing a login form

Let’s take a step forward and practice some real-world examples. We will test a form that takes email and password inputs from a user and passes them to the onSubmit method. We will also assert that it renders a validation error if a user has not entered a password. Here’s our component:

Code for the Email Form Component

Let’s test our first use case: the component should display a “password required” error. Here’s our test will look:

Code for the Email Form Component Test

Here we are testing an action that has an impact on state. Since setState is async in React, we have to execute this test in an async way. RNTL provides the waitFor API for this. Also, we have used the same fireEvent API here for changing the text of our input component.

Moving to our next use case, we expect to call onSubmit with email and password:

Mocking the Function Argument in the Form Component Test

We’re taking another step forward by asserting the mock function’s argument to make sure we have the expected output. Here is our full test suite for the EmailPasswordForm component.

Building our testing flow

Now that we’ve built up some confidence, let’s test our complete login flow. This isn’t just about testing methods being called. Login usually includes two main functions: it has some network API call that returns a token, and then we save that token in local storage for later use.

Mocking API

In order to make our test independent of server/network, we will mock our API hits. This is possible with the fabulous open source package fetch-mock-jest. It intercepts all fetch calls and resolves them with given response. This allows us to test all success and error cases. Let’s add it to our code base.

yarn add -D fetch-mock-jest

Here is the simplest mocking we will use; it mocks a login API call with an expected response.

Mocking a Login API Call

Mocking AsyncStorage

React Native apps use a number of native bridges to access platform-specific functionalities. In our case, we have to make sure the login API response is stored in local storage.

RNTL allows us to mock these native bridges so we can run our tests without depending on devices or emulators. In order to mock these libraries, we will add the file jest.mock.js, which will code for mocking all of the native libraries we are using in our app. Here’s our mock file:

jest.mock('@react-native-community/async-storage', () => ({
  setItem: jest.fn(),
}));

One important point to note here: we have to mock all methods that we are using from libraries, as these are not added in our testing environment.

Now we will reference this file from our jest.configs.js file:

module.exports = {
  ...
  setupFiles: ['./jest.mock.js']
  ...
};

Adding this file to the setupFiles array will tell Jest to execute this file before running any test suite.

Full testing suite

Now that we have our mocking in place, let’s take a look at how we’ll test our full flow:

Full Test Flow for Our Login Screen

Don’t worry — it’s just lengthy, not hard to digest. We already know most of this.

To begin with, we have added fetchMock in our beforeAll hook to make sure our fetch mocking begins before this test suite starts. Here we have rendered our screen component with a mocked navigate method; this is how our screen will be rendered in the app.

After executing all user interactions, we start asserting things happen as we expect. Here is the list of our expectations:

  • API is called with expected values
  • App navigates to homescreen once API response is received
  • jwtToken is stored in local storage

If all of these tests pass, we are sure that our login screen works the way we expect it to. This marks our login flow testing as complete.

Pre-test task: Linting

Testing might take a bit longer to execute over time as our test suites grow in number and size. Linting our code before executing any tests will make sure we are not executing tests on code with any obvious bugs. We will add lint tasks in the pre-test script. Here’s what it looks like:

Adding the Pre-test Linter

You can view the full package.json file here.

Post-test task: Adding a reporter

Who doesn’t like charts and dashboards to visualize performance? Here, we will integrate the Jest test reporter, which will deliver us a complete picture of our test execution. This will help us monitor what tests are breaking and how much time each test takes to complete.

Adding the reporter to our project:

yarn add -D jest-html-reporters

Next up, let’s add this reporter to our jest.configs.js file. Here’s how it looks:

Adding Reporter to Our Configs File

To get it running, let’s open up this reporter automatically every time our tests are executed. It’s simple — we’ll add it to our post-test script as an update to our package.json:

 

Adding Reporter in the Post-test Script

Here’s the test report for our project:

Visualization of the Project Test Report

And here’s the .html file of the report.

Adding pre-commit hooks

For large-scale applications (and side projects that we work on every seventh day), it’s better to have solid checks in place to maintain code quality. These checks must be enforced to ensure that every PR that arrives in our repos is up to standard and passes defined criteria.

Pre-commit hooks can help us here. These hooks run before every commit. We can add our test task to make sure that no commit is allowed to break any of our tests. We are using husky here, which makes adding commit hooks much easier. This is our last library to add:

yarn add -D husky

Let’s set up our first pre-commit hook here. npx will help us with that:

npx husky add pre-commit "yarn test" # will create .husky/pre-commit file

Running this command will create a .husky/pre-commit file for us. It’s a bash script that will be executed before any commit.

At times, our list tasks have a --fix option that makes changes to our files as we run the command. In our case, we added a Jest reporter in the section above, which will create a new reporting file as we run our tests. In order to add all of these changes to our commit after pre-commit hooks are executed, we will update our pre-commit file. This is how it looks:

Final Update to Our Pre-commit File

And we’re done for today!

Yes, I know how it feels when you have a complete testing ecosystem in place — more relaxed and confident that your code won’t break too soon.

React Native Testing Library makes it easier to follow best practices and keeps your testing experience joyful. It allows you to test your application in the same ways your users interact with it. Making sure all your use cases are covered provides a lot of confidence — you better believe it will get your app’s rating up.

Moving forward from here, you can try out Detox for end-to-end testing; it will help you test for cases that require native APIs, like notifications, deep linking, and sophisticated user interactions.

Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with mobile machine learning, React, React Native, and UI designing.

Leave a Reply