Getting started unit testing in React native applications with Jest & Testing Library
What is the React Native Testing Library ?
The React Native Testing Library (RNTL) is a lightweight solution for testing React Native components. It provides light utility functions on top of react-test-renderer
, in a way that encourages better testing practices.
Installation:
Open a Terminal in your project’s folder and run:
npm install --save-dev @testing-library/react-native
or
yarn add --dev @testing-library/react-native
This library has a peerDependencies listing for react-test-renderer
and, of course, react
. Make sure to install them too!
To using Jest:
Add @testing-library/jest-native
package to your project, in order to use addtional React Native-specific jest matchers :
npm install --save-dev @testing-library/jest-native
or
yarn add --dev @testing-library/jest-native
Then automatically add it to your jest tests by using setupFilesAfterEnv
option in your Jest configuration (it’s usually located either in package.json
under "jest"
key or in a jest.config.js
file):
{
"preset": "react-native",
"collectCoverage": true,
"coveragePathIgnorePatterns": ["/node_modules/", "/jest"],
"setupFilesAfterEnv": ["@testing-library/jest-native/extend-expect"],
"transformIgnorePatterns": ["node_modules/(?!(jest-)?react-native|@?react-native)"]
}
- collectCoverage : indicates whether the coverage information should be collected while executing the test. Because this retrofits all executed files with coverage collection statements, it may significantly slow down your tests. For example :
coveragePathIgnorePatterns
: An array of regexp pattern strings that are matched against all file paths before executing the test. If the file path matches any of the patterns, coverage information will be skipped.
- setupFilesAfterEnv : A list of paths to modules that run some code to configure or set up the testing framework before each test file in the suite is executed.
- transformIgnorePatterns : An array of regexp pattern strings that are matched against all source file paths before transformation. If the file path matches any of the patterns, it will not be transformed.
Create component:
Let’s create a QuestionsBoard component to illustrate and simulate a unit test :
//QuestionsBoard.js
import * as React from 'react';
import {
View,
TouchableOpacity,
Text,
ScrollView,
TextInput,
} from 'react-native';
function QuestionsBoard({ questions, onSubmit }) {
const [data, setData] = React.useState({});
return (
<ScrollView>
{questions.map((q, index) => {
return (
<View key={q}>
<Text>{q}</Text>
<TextInput
accessibilityLabel="answer input"
accessibilityHint="input"
onChangeText={(text) => {
setData((state) => ({
...state,
[index + 1]: { q, a: text },
}));
}}
/>
</View>
);
})}
<TouchableOpacity onPress={() => onSubmit(data)}>
<Text>Submit</Text>
</TouchableOpacity>
</ScrollView>
);
}
export default QuestionsBoard;
Test the functioning of the components:
In a QuestionsBoard-test.js file, Let’s test components to see what unit testing is about..
//QuestionsBoard-test.js
import 'react-native';
import React from 'react';
import App from '../App';
import { render, fireEvent } from '@testing-library/react-native';
describe('QuestionsBoard', function () {
it('should display snapshoot', function () {
const allQuestions = ['q1', 'q2'];
const { toJSON } = render(
<App questions={allQuestions} />
);
expect(toJSON()).toMatchSnapshot();
});
});
This test is the snapshot test that captures the rendering of the component in a file. When you run yarn test
or npm run test
, this will produce an output file like this:
// QuestionsBoard-test.js.snap
exports[`QuestionsBoard should display snapshoot 1`] = `
<RCTScrollView>
<View>
<View>
<Text>
q1
</Text>
<TextInput
accessibilityHint="input"
accessibilityLabel="answer input"
allowFontScaling={true}
onChangeText={[Function]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
/>
</View>
<View>
<Text>
q2
</Text>
<TextInput
accessibilityHint="input"
accessibilityLabel="answer input"
allowFontScaling={true}
onChangeText={[Function]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
/>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<Text>
Submit
</Text>
</View>
</View>
</RCTScrollView>
`;
These snapshot tests prevent regressions by comparing the actual characteristics of an application or component with the “correct” stored values for those characteristics. Snapshot tests are fundamentally different from unit and functional tests.
Our second test is the interaction test. We have in our QuestionsBoard.js file, a controlled text input (TextInput) that calls the prop onChangeText and a wrapper (TouchableOpacity) that calls the prop onSubmit. In order to test it, provide mock functions to these props:
//QuestionsBoard-test.js
import 'react-native';
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import QuestionsBoard from '../src/QuestionsBoard';
describe('QuestionsBoard', () => {
it('should display snapshoot', () => {
const allQuestions = ['q1', 'q2'];
const mockFn = jest.fn();
const { toJSON } = render(
<QuestionsBoard questions={allQuestions} onSubmit={mockFn} />
);
expect(toJSON()).toMatchSnapshot();
});
it('form submits two answers', () => {
const allQuestions = ['q1', 'q2'];
const mockFn = jest.fn();
const { getAllByA11yLabel, getByText } = render(
<QuestionsBoard questions={allQuestions} onSubmit={mockFn} />
);
const answerInputs = getAllByA11yLabel('answer input');
fireEvent.changeText(answerInputs[0], 'a1');
fireEvent.changeText(answerInputs[1], 'a2');
fireEvent.press(getByText('Submit'));
expect(mockFn).toBeCalledWith({
'1': { q: 'q1', a: 'a1' },
'2': { q: 'q2', a: 'a2' },
});
});
});
Let’s conclude with the following result, which shows us that the components and props implemented in our QuestionsBoard.js file have been 100% covered .