Guide to React Native Testing Library

kahmun
AIA Singapore Technology Blog
12 min readJun 22, 2023

Often, testing is part of the software development lifecycle. It is important for a several reasons:

  1. It helps to catch bugs before deploying to the production and prevent issues to end users.
  2. It allows us to think carefully about the quality of the code.
  3. It boosts our confidence that our code is worked as expected.
Photo by Kelly Sikkema on Unsplash

Types of testing for front-end application 🧚‍♂️

  1. Unit testing: It typically focuses on verifying the code works correctly, such as providing inputs to a function and checking its output against expected results.
  2. Component testing: It focuses on verifying an individual component renders correctly and behaves as expected based on the provided inputs to it.
  3. Integration testing: It tests how multiple components interact with each other, generally involves rendering multiple components and requires external dependencies, services or APIs. It is required to mock these for tests.
  4. Snapshot testing: It captures a snapshot of the rendered output of a component and compares it to previous saved snapshot to detect visual changes. It should be used with in combination with unit and component testing to ensure comprehensive coverage of an application.
  5. End to end testing (E2E): It typically verifies the entire application from start to end to ensure the user flows work correctly, this can include testing a user can sign up, login, perform activities, navigate to different pages and log out from an application.

Installing dependencies 🧚‍♂️

React Native Testing Library is designed for testing React Native components, it has peerDependencies listing for react-test-renderer, hence we need to first install it.

npm install --save-dev react-test-renderer

Next, install React Native Testing Library.

npm install --save-dev @testing-library/react-native

There is a specific jest matcher for React native which is jest native library that provides additional matchers and utilities to test properties of React Native components.

npm install --save-dev @testing-library/jest-native

Setting it up 🧚‍♂️

Usually creating a new React Native project will generate a default jest.config.js with some basic configuration. You can find it in the root directory of your project.

{
"preset": "jest-expo",
"setupFiles": [
'./src/mock/api',
'./src/mock/services',
],
"setupFilesAfterEnv": ["@testing-library/jest-native/extend-expect"],
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*)"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
],
"moduleNameMapper": '^@/(.*)$': '<rootDir>/src/$1',
}

This is an example of the customize Jest’s configuration for React Native project based on our testing needs, which includes:

  1. preset: jest-expo preset is used for testing React Native projects created using Expo, otherwise it would be react-native.
  2. setupFiles: This option is used to specify a module that should be executed before any tests run, such as setting up the test data or mocking external components, dependencies, services or APIs.
  3. setupFilesAfterEnv: This option is used to set up additional testing libraries such as @testing-library/jest-native.
  4. transformIgnorePatterns: This option is used to configure a list of file paths that should not be transformed, such as third party library files.
  5. moduleFileExtensions: This option is used to specify the file extensions that should be looked for when running tests.
  6. moduleNameMapper: This option is used to specify the mapped modules name with absolute or relative paths.

Let’s take a look at the following commands to run a test file,

// run a single test file
jest <path-to-test-file>

// run all test files in a folder
jest <folder-path>

// update snapshot
jest --updateSnapshot <path-to-test-file>

How to write test case for a component 🧚‍♂️

A test suite is a collection of test cases that are designed to test a specific behavior of a system. But how do we write a test suite for a component that ends up an UI to end-users?

  1. Identify different states and behaviors of a component. This may include loading state, error state (when the data is not returned), or when the data is ready to be displayed on the UI. You may need to mock external services or APIs to retrieve the data.
  2. Write test cases for each behaviors. Once you have identified different behaviors of the component, you can start writing test cases to verify the component is worked as expected. Take an example, you may need to verify that the loading screen is displayed once an user hits on a section, the data is correctly displayed after the loading, display an error message when the input is invalid, or successfully navigates to the next screen.
  3. Run the tests and debug as needed once you have written your test cases. You can use the debug method from the React Native Testing Library check for any unexpected behaviors.
  4. Continually updating the tests as you continue to develop the component to make sure all different states and behaviors are covered.

The below example shows how a test suite looks like.

import React from 'react';
import { render } from '@testing-library/react-native';

describe('Render <Component /> correctly', () => {
it('matches the snapshot', () => {
// includes test function to match a snapshot
});

it('renders error screen when the data is invalid', () => {
// expect assertion for error state
});

it('shows loading indicator when the data is being fetch', () => {
// expect assertion for loading state
});
})

React Native Testing Library 🧚‍♂️

React Native Testing Library (RNTL) is a testing library specifically designed for testing React Native components. It provides a set of utilities for rendering a component, querying a single element as well as simulating user interaction. Let’s go through a few essential APIs by React Native Testing Library.

render() creates a virtual representation of a component and returns various of queries methods to test the behavior of a component.

describe('Render <Component /> correctly', () => {
it('shows loading indicator when the data is being fetch', () => {
const { getByTestId, debug } = render(<Component/>);
debug();
});
})

debug() destructs the variable from the render method that is used to print the HTML structure of a component to the console. This is useful for debugging any failures or unexpected behavior of a component.

waitFor() waits for the certain conditions to be true before continuing the next statement, this is useful when testing for asynchronous behavior of a component, i.e waiting for an element to be appear or disappear from the DOM. The duration of the timeout can be adjusted, default timeout is 5000ms.

Furthermore, you can use fireEvent to simulate user interactions by triggering the change and input events.

There are a total of six query methods for querying the matching element,

  • getBy: Returns the first element that matches the query, throws error if no elements match.
  • getAllBy: Returns an array of all elements that match the query, throws error if no elements match.
  • queryBy: Returns the first element that matches the query, returns null if no element match.
  • queryAllBy: Returns an array of all elements that match the query, returns empty array if no elements match.
  • findBy: Returns a promise that resolves to the first element that matches the query, throws error if no elements match.
  • findAllBy: Returns a promise that resolves to an array of elements that match the query, throws error if no elements match.

The table below shows the summary of the query methods,

Followed by commonly used query options, as in

  • ByText: Returns an element with matching text content.
  • ByTestId: Returns an element with matching test ID of a component.
  • ByPlaceholderText: Returns TextInput element with matching placeholder text.
  • ByDisplayValue: Returns TextInput element with matching display value.

The below table shows the query methods with options that are used to search for elements in the component tree based on the text content, test ID, placeholder or display value.

Here is an example of component and test suite to demonstrate the usage of React Native Testing Library’s API.

// Component.js
import React from 'react';
import { FlatList, TouchableOpacity } from 'react-native';
import Item from './Item';

const data = [
{ id: '1', text: 'Text 1' },
{ id: '2', text: 'Text 2' },
{ id: '3', text: 'Text 3' },
];

const Component = ({ onItemPress }) => {
const renderItem = ({ item }) => (
<TouchableOpacity onPress={() => onItemPress(item.id)}>
<Item key={item.id} title={item.text} />
</TouchableOpacity>
);

return (
<FlatList
testId="flat-list"
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
);
};

export default Component;


// __tests__/Component.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Component from '../Component';

describe('render <Component />', () => {
it('renders items correctly', () => {
const { getByTestId } = render(<Component />);
const list = getByTestId('flat-list');

expect(list).toBeDefined();

// expect the data length to be 3
expect(list.props.data.length).toBe(3);
});

it('displays the correct item', () => {
const { getByText } = render(<Component />);
const text1 = getByText('Text 1');
const text2 = getByText('Text 2');
const text3 = getByText('Text 3');

expect(text1).toBeDefined();
expect(text2).toBeDefined();
expect(text3).toBeDefined();
});

it('onPressItem is called when pressed', () => {
const onItemPressMock = jest.fn();
const { getByText, getByTestId } = render(<Component onItemPress={onItemPressMock}/>);

// find the flat-list and Text elements
const list = getByTestId('flat-list');
const text1 = getByText('Text 1');

// simulate the press on 'Text 1'.
fireEvent.press(text1);

// the onItemPressMock handle is called with the 'id' of the item passed as an argument
expect(onItemPressMock).toHaveBeenCalledWith('1');
});
});

Jest Native 🧚‍♂️

Jest Native is a library that provides additional matchers that make it easier to test React Native components.

Refer to this site for summary usage for each of the utility function.

Rewrite the above test suite with jest-native by using toBeOnTheScreen and toHaveTextContent matchers, as below.

// __tests__/Component.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Component from '../Component';

describe('render <Component />', () => {
it('renders the item correctly', () => {
const { getByTestId } = render(<Component />);
const list = getByTestId('flat-list');

expect(list).toBeOnTheScreen();

// expect the data length to be 3
expect(list.props.data.length).toBe(3);
});

it('displays the correct item', () => {
const { getByText } = render(<Component />);
const text1 = getByTestId('text-1');
const text2 = getByTestId('text-2');
const text3 = getByTestId('text-3');

expect(text1).toHaveTextContent('Text 1');
expect(text2).toHaveTextContent('Text 2');
expect(text3).toHaveTextContent('Text 3');
});
});

While it is possible to use jest without jest-native, using jest-native makes testing React Native apps more efficient and easier to maintain, especially when testing specific React Native components.

Jest 🧚‍♂️

This is a common JavaScript testing framework that provides a test runner, assertion library and mocking library. It covers a variety of use cases, from basic equality testing to more complex behaviors.

Mocking is particularly important for a complex application that rely on external services, APIs and components. We can isolate the component being tested and ensure that it is functioning correctly.

Mocking component

There are several ways to mock an external components:

  1. Create a __mocks__ directory under the same directory as the ChildComponent.js file and create a file named ChildComponent.js in the __mocks__ directory, as below.
// __mocks__/ChildComponent.js
import React from 'react';
import { View } from 'react-native';

const MockedChildComponent = ({ children }) => {
return <View testID="mocked-child-component">{children}</View>;
};

export default MockedChildComponent;


// __tests__/Component.test.js
import React from 'react';
import { render } from '@testing-library/react-native';
import Component from './Component';
import MockedChildComponent from './__mocks__/ChildComponent';

// mock the child component
jest.mock('./ChildComponent', () => MockedChildComponent);

describe('Component', () => {
it('renders the mocked child component correctly', () => {
const { getByTestId } = render(<Component />);

expect(getByTestId('mocked-child-component')).toBeOnTheScreen();
});
});

2. Mock the external component directly on the test file.

// __tests__/Component.test.js
import React from 'react';
import { render } from '@testing-library/react-native';
import Component from './Component';

// mock the child component
jest.mock('../ChildComponent', () => {
return {
__esModule: true,
default: ({ children }) => {
return <mocked-child-component>{children}</mocked-child-component >;
},
};
});

describe('Component', () => {
it('renders the mocked child component correctly', () => {
const { getByTestId } = render(<Component />);

expect(getByTestId('mocked-child-component')).toHaveTextContent('Test');
});
});

Both methods are valid and they work in different scenarios. The first method is useful when the external component is used in multiple components, so just mock it and use it in multiple tests. The second method is useful when we need to mock the child component for a single test.

Mocking API

// api.js
export const getData = async () => {
const response = await fetch('https://example.com', {
method: 'GET
});
return response;
};

// Component.test.js

import { getData } from './api';
import Component from './Component';

const data = { id: 1, text: 'Text 1' };
jest.mock('./api', () => ({
getData: jest.fn().mockReturnValue(data),
}));

describe('Component', () => {
it('displays the correct data', async () => {
const { getByText } = render(<Component />);
const text1 = getByTestId('text-1');

expect(text1).toHaveTextContent('Text 1');
});
});

The code above shows when the API is called when the component is active, it will use the mocked API instead of the real implementation, allowing us to test the component that displays the correct data without making any network requests. In this case, mockReturnValue is used for defining the value the mocked function returns.

Mocking function

In order to mock a function, using jest.fn() can create a mock function to replace the real function in our code during testing.

We don’t need to mock every function, only functions that have external dependencies or side effects need to be mocked to ensure that the tests focus only on its behavior but not the behavior of its dependencies.

Testing a function without mocking,

// utils.js
export const add = (a, b) => {
return a + b;
}

// utils.test.js
import { add } from './utils';

describe('add function', () => {
it('should add two numbers correctly', () => {
expect(add(1, 1)).toEqual(2);
});
});

Testing a function with mocking,

// Component.js
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';

const Component = () => {
const handlePress = () => {
console.log('Button pressed!');
};

return (
<TouchableOpacity onPress={handlePress}>
<Text>Button</Text>
</TouchableOpacity>
);
};

export default Component;

// Component.test.js

import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Component from './Component';

describe('Component', () => {
it('calls handlePress when button is pressed', () => {
const handlePress = jest.fn();
const { getByText } = render(<Component handlePress={handlePress} />);
const button = getByText('Button');
fireEvent.press(button);
expect(handlePress).toHaveBeenCalled();
});
});

In this case, the mock function is created for the handlePress to test the functionality of the handlePress independently to the component. Therefore, we are able to ensure that it is worked as expected.

jest.fn() replaces a function and we can assert that the function was called with specific arguments, was it being called, or how many times it was called. This way, you can test the behavior of a component without depending on external resources.

Snapshot testing 🧚‍♂️

This is another technique for testing a component by capturing a snapshot of a component and compare it to previous snapshots to detect for any unexpected changes.

// Component.js
import React from 'react';
import { render } from '@testing-library/react-native';
import Component from '../MyComponent';

describe('Component', () => {
it('renders component correctly', () => {
const { toJSON } = render(<MyComponent />);
expect(toJSON()).toMatchSnapshot();
});
});

// Component.test.js.snap

exports[`renders component correctly`] = `
<View
style={
Object {
"backgroundColor": "black",
}
}
>
<Text
style={
Object {
"color": "white",
"fontSize": 12,
}
}
>
Text 1
</Text>
</View>
`;

In this example, toMatchSnapshot() method is used to compare the rendered output to a saved snapshot of the component. It will create a snapshot of the rendered output in a file if we run it for the first time.

If the rendered output matches the snapshot, then the test passes. otherwise jest will display the changes and prompt to update the snapshot if necessary.

By using the React Native Testing Library in conjunction with Jest Native and Jest can provide a comprehensive testing for React Native components.

The combination of these tools make testing component rendering and behaviors much better. In overall, it could improve code coverage, boost our confidence in any code changes by catching bugs or failures before making it to production.

For your information, React Native Testing Library does not support end-to-end testing, but it is still possible to use it in combination with other tools (e.g Cypress or Detox) to perform end-to-end testing.

There are some thoughts when it comes to whether we should write tests or build a component first.

Test-driven development (TDD) is one of the development method which involves writing tests before writing the code to ensure the overall code is well-designed and testable. The other way is to write code first before adding the tests, but it may lead to poor code coverage.

Nonetheless, there are always pros and cons on every approach. Utilising the TDD method can leads to a better quality of code, while the latter is useful for a small to mid sized project as the code coverage can be easily improved. The best approach is always depends on the size of the project and try to aim for a balance between writing code and tests. 😉

--

--

kahmun
AIA Singapore Technology Blog

Full Stack Engineer | Powering User Experience | Architecting Robust Systems 🌏 Malaysia