How to Test React Native Apps
Tags: reactnative, testing, jest, mock
Published: 2022-06-23 20:06:49

## Introduction

Have you ever had a bug introduced by solving another problem and didn't realize it until weeks later? This could have been easily avoided if you had tests for your App.

In a project with a tight schedule, tests are generally the first thing to be forgone, but they are being underestimated. Once you make that click and understand how to create tests, it's not that complicated to add them and avoid future problems.

## Overview

In this article, you will learn how to test React Native Apps using [jest](https://jestjs.io/docs/en/getting-started) and [@testing-library/react-native](https://www.npmjs.com/package/@testing-library/react-native).

## About @testing-library/react-native

One important thing to point out is that this testing library has changed many times and migrations were needed from one version to another. This tutorial will work with the latest version at the time of writing it (`7.0.1`). For migrations, you can check their [migration guide](https://callstack.github.io/react-native-testing-library/docs/migration-v7/#guide-for-testing-libraryreact-native-users).

## App to be tested

For this tutorial, we are going to use [this](https://github.com/ManuViola77/testing_react_native_apps) simple app, but of course, you can apply everything learned in any app you want. In this app, we just have two simple screens with some components we can test.

## Configuration

Since we are going to use the `@testing-library/react-native` library, we need to install it. To use the specific `7.0.1` version we can install it like this:
`npm install @testing-library/react-native@7.0.1`

We also need to configure jest. We should create a `jest.config.js` file in the projects root folder and for our example, we can fill it with this content:

```js
module.exports = {
  preset: 'react-native',
  modulePathIgnorePatterns: ['extras'],
  setupFiles: [
    '<rootDir>/node_modules/react-native-gesture-handler/jestSetup.js',
  ],
  transformIgnorePatterns: [
    'node_modules/(?!(jest-)?react-native|react-(native|universal|navigation)-(.*)|@react-native-community/(.*)|@react-navigation/(.*)|bs-platform|@rootstrap/redux-tools)',
  ],
  verbose: true,
};
```
I will mention some of these configurations later in this tutorial and you can find more information [here](https://jestjs.io/docs/en/configuration).

## Tests folder

As you can see, our project already has a `__tests__` folder with an `App-test.js` file inside. This tests if the app is created and rendered correctly.

## Creating tests

To actually create a test, we should create a file with a `.spec.js` extension inside the `__tests__` folder. We could even add folders inside to better organize the tests, for example, add a folder called `screens` and have one test per screen inside.

Inside these tests, we are going to use `describe()` just to describe what we want to test, `it()` (same as `test()`) to create the tests we want to have, `expect()` to assert our expected behavior and `beforeEach()` to execute some code before each `it()` test. [Here](https://jestjs.io/docs/en/api) is a complete list of `jest` methods to use inside tests.

Correspondingly, we use the `@testing-library/react-native` library to test our components behavior. We could use `fireEvent` to fire a button pressed event or `waitFor()` to wait for promises results. [Here](https://www.native-testing-library.com/docs/cheat-sheet) is a cheat sheet with the possible methods to use.

Notice that to know how to ask for a specific component, we can simply add the `testID="componentId"` property to the component we want to identify. To simplify, I already added this property to all the components we are testing.

## Creating tests for our App

Now we need to create our own tests, so we can test our `MainScreen` and `SecondaryScreen`. We can create the `screens` folder inside the `__tests__` folder to have better organized tests.

Also, we are going to need some extra files to handle navigation and screen parameters data, so we can create an `extras` folder inside the `__tests__` folder for this purpose. If you paid attention you may have noticed that we added `modulePathIgnorePatterns: ['extras']` to the `jest.config.js` file. This is to tell jest to ignore the `.js` files that this folder contains, otherwise, it will consider them as tests and we don't want that.

### Navigation helper

To test our app we need to start on some screen. If we use a simple `render AppStack` we would always start on the same screen and we would need to have some flow to go to a different one. To avoid this, we could just start our stack with the screen we want and if we need to test our screen redirecting to some other screen we just need to add that other screen to the stack for the test to work.

Keeping this in mind, we will create a `helpers.js` file inside the `extras` folder that looks like this:

```js
import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import {render} from '@testing-library/react-native';

const Stack = createStackNavigator();

const renderOtherComponents = (otherComponents, screenConfig = {}) => {
  return otherComponents.map(({name, component}) => {
    return (
      <Stack.Screen
        {...screenConfig}
        key={name}
        name={name}
        component={component}
      />
    );
  });
};

export const renderWithNavigation = (
  mainComponent,
  {otherComponents = [], navigatorConfig = {}, screenConfig = {}} = {},
) => {
  const App = () => (
    <NavigationContainer>
      <Stack.Navigator {...navigatorConfig}>
        <Stack.Screen
          {...screenConfig}
          name="TestNavigator"
          component={mainComponent}
        />
        {otherComponents &&
          renderOtherComponents(otherComponents, screenConfig)}
      </Stack.Navigator>
    </NavigationContainer>
  );

  return {...render(<App />)};
};
```
We will get more into detail when we use it in the `SecondaryScreen` test.

### MainScreen test

Inside the `screens` folder, we should create the `MainScreen.spec.js` file.

Since we are in the AppStack's first screen we can just render the entire `AppStack` and it would start rendering the screen we want.

This file should test the `MainScreen` components existence and behavior:

- It renders the `MainScreen` component
- It renders the `button-to-secondary-screen` component
- When pressing `button-to-secondary-screen` component, it redirects to the `SecondaryScreen` component
- It renders the `alert-button` component
- When pressing `alert-button` component, it shows an alert

Each of these items should translate to an `it()` jest method and so our `MainScreen.spec.js` file should look like this:

```js
import {act, fireEvent, waitFor} from '@testing-library/react-native';
import {Alert} from 'react-native';
import AppStack from '../../src/navigators/AppStack';

import {renderWithNavigation} from '../extras/helpers';

describe('<MainScreen />', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = renderWithNavigation(AppStack);
  });

  it('should render the main screen', () => {
    expect(wrapper.queryByTestId('MainScreen')).toBeTruthy();
  });

  it('should render the go to secondary screen button', () => {
    expect(wrapper.queryByTestId('button-to-secondary-screen')).toBeTruthy();
  });

  describe('when go to secondary screen button is pressed', () => {
    it('it should render SecondaryScreen ', async () => {
      act(() => {
        fireEvent.press(wrapper.queryByTestId('button-to-secondary-screen'));
      });
      await waitFor(() =>
        expect(wrapper.queryByTestId('SecondaryScreen')).toBeTruthy(),
      );
    });
  });

  it('should render the alert button', () => {
    expect(wrapper.queryByTestId('alert-button')).toBeTruthy();
  });

  describe('when take alert button is pressed', () => {
    it('it should render an Alert ', async () => {
      const alertSpy = jest.spyOn(Alert, 'alert');
      act(() => {
        fireEvent.press(wrapper.queryByTestId('alert-button'));
      });
      await waitFor(() => expect(alertSpy).toHaveBeenCalled());
    });
  });
});
```

### SecondaryScreen test

Inside the `screens` folder we should create the `SecondaryScreen.spec.js` file.

Since we are not in the AppStack's first screen and our screen receives parameters, we are going to take advantage of the helper we created. We will call the function `renderWithNavigation` with these parameters:

- `mainComponent`: `SecondaryScreen`
- `otherComponents`: `[{name: 'MainScreen', component: MainScreen}]`
- `screenConfig`: `{initialParams: { screenParameters }}`

We will create a `data.js` file inside the `extras` folders to set our `screenParameters`:

```js
export const screenParameters = {
  paramOne: 'This is a parameter that is a text',
  paramTwo: {
    content: 'This is a parameter that is an object with a content',
  },
};
```
This file should test the `SecondaryScreen` components existence and behavior:

- It renders the `SecondaryScreen` component
- It renders the `back-button` component
- When pressing `back-button` component, it redirects to the `MainScreen` component
- It renders the `title` component
- It renders the `param-one` component
- It renders the `param-two-content` component

Notice that our `param-one` and `param-two-content` only render if they have content to render, so if our tests pass means that the screen is getting the parameters right!

Our `SecondaryScreen.spec.js` file should look like this:

```js
import {act, fireEvent, waitFor} from '@testing-library/react-native';

import {screenParameters} from '../extras/data';
import {renderWithNavigation} from '../extras/helpers';
import SecondaryScreen from '../../src/screens/SecondaryScreen';
import MainScreen from '../../src/screens/MainScreen';

describe('<SecondaryScreen />', () => {
  let wrapper;

  const otherComponents = [{name: 'MainScreen', component: MainScreen}];

  beforeEach(() => {
    wrapper = renderWithNavigation(SecondaryScreen, {
      otherComponents,
      screenConfig: {
        initialParams: screenParameters,
      },
    });
  });

  it('should render SecondaryScreen', () => {
    expect(wrapper.queryByTestId('SecondaryScreen')).toBeTruthy();
  });

  it('should render the go back button', () => {
    expect(wrapper.queryByTestId('back-button')).toBeTruthy();
  });

  describe('when go back button is pressed', () => {
    it('it should render MainScreen ', async () => {
      act(() => {
        fireEvent.press(wrapper.queryByTestId('back-button'));
      });
      await waitFor(() =>
        expect(wrapper.queryByTestId('MainScreen')).toBeTruthy(),
      );
    });
  });

  it('should render title', () => {
    expect(wrapper.queryByTestId('title')).toBeTruthy();
  });

  it('should render param one', () => {
    expect(wrapper.queryByTestId('param-one')).toBeTruthy();
  });

  it('should render param two content', () => {
    expect(wrapper.queryByTestId('param-two-content')).toBeTruthy();
  });
});
```

## Running tests

To run the tests we just simply need to execute `npm test`. This command will run all the tests we have under the `__tests__` folder. If we want to just run one particular test, we can do `npm test __tests__/screens/one_particular_test.js` instead.

We can see how our tests run successfully:

![Our tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8nl93txlxdf8k8sq6s2.png)

## Debugging tests

If we want to debug our tests we can use [ndb](https://www.npmjs.com/package/ndb).

To use this, first, we need to install it, as the documentation explains, by running `npm install ndb` and you can add the `-g` option to make it global.

Then, we can configure our project by setting the value `"test:debug": "ndb jest",` inside our `package.json` file under the `"scripts"` section (we can put it right after the `"test"` value).

Finally, we can debug our tests by running `npm run test:debug` and this will install Chromium where you will be able to set breakpoints in your tests and debug them.

## Mocks

Since our app was really simple, we didn't need to implement mocks, but it's really an important part of testing react native apps. I will explain why we need them and how we can create them.

The first thing you need to know is that there are lots of functionalities you will have to mock in order to use and test. For this purpose, you can create a `__mocks__` folder inside the `__tests__` one. Every file in there will be named after a react native library, so when you are testing something that uses that library, jest will take and use the implementation made in the mock.

Let's exemplify. Suppose that in our project we have a backend we access through a URL that we have saved in an `.env` file with the name `API_URL` and we use the library `react-native-config` to access that information. Our tests won't be able to access that `.env` file, so instead, we have to mock it. We would create (if we haven't already) the `__mocks__` folder and inside we would create a file named `react-native-config.js` that contains the URL we want, like this:

```js
export default {
  API_URL: 'https://mock.com/api'
};
```

A slightly more complex example would be if we use an `ImagePicker` to access our photos gallery. Let's say we use the `react-native-image-crop-picker` library and in our code we call `ImagePicker.openPicker()` expecting a selected image as a result. If our test calls the button that opens the picker expecting a selected image as a result but we don't have the picker mocked, our test would fail, getting a null result, which is not what we expected. So, we have to create our `react-native-image-crop-picker.js` file and inside we could have something like this:

```js
export default {
  openPicker: jest.fn().mockImplementation(() => Promise.resolve(result))
};
```

What we are doing here is mocking our `openPicker` function, by saying it is a function (`jest.fn()`) and that its mocked implementation would be `Promise.resolve(result)`. Notice that since our real `openPicker` is a promise, our mocked implementation is also a promise. We can decide if we want our mocked promise to be completed successfully (with `Promise.resolve()`) or if we want it to fail (with `Promise.reject()`). We also decide if we want to return something, like in this case, we are returning `result`. A question you might be asking is, how do I test both fail and success results by only mocking one `openPicker` function? Well, the answer is quite simple actually, and it would look like this:

```js
export default {
  openPicker: jest
   	.fn(() => Promise.resolve(result)) // default implementation
   	.mockImplementationOnce(() => Promise.reject(result)) // first time is called
   	.mockImplementationOnce(() => Promise.resolve(result)), // second time is called
};
```

By using the method `mockImplementationOnce()` we can give different behaviors to our function, depending on each time you call it. So in this case, the first time we call it we would get a rejected promise (for example to mock that we don't have access to the gallery yet), the second time would be a successful case with our expected result and if we were to call it more times it would use the default implementation inside the `fn()` method, which in this case would also be successful.

## Summary

In this tutorial, we saw how to use `jest` and `@testing-library/react-native` to test React Native Apps. You can find the complete GitHub project (with tests included) [here](https://github.com/ManuViola77/testing_react_native_apps_with_tests).

Read this article and more content in the Rootstrap blog: https://www.rootstrap.com/blog/how-to-test-react-native-apps/