React Native has emerged as a popular choice in the cross-platform mobile application space, as it saves between 40% and 90% of time spent developing apps on different platforms. As your application grows and the codebase expands, small errors and edge cases you don’t expect can cascade into larger failures. Bugs lead to bad user experiences and, ultimately, business losses. One way to prevent fragile programming is to test your code before releasing it into the wild.
In this article, we will cover how to test custom reusable components in React Native. Let’s get started!
Written by Sneh Pandya
Set up React Native application
Let’s get started by setting up our project. To create a new application, run the commands as follows:
npm install -g expo-cli // To add Expo CLI on your machine
npx react-native init ReactNativeComponentTesting // To set up a new React Native application
cd ReactNativeComponentTesting
Next, we will install testing dependencies, such as jest
and react-test-renderer
.
Let’s install the dependencies by running the command below in the terminal:
npm install react-test-renderer // React Test Renderer
npm install --save-dev jest // Jest
npm install --save-dev @testing-library/react-native @testing-library/jest-native // React Native Testing library
Create a reusable component
Let’s write a reusable React Native Button and test it. Suppose the reusable Button component contains the specifications below:
Fixed height and width with density-independent pixels
Primary buttons should have a blue background and white text
Secondary buttons should have a red background and white text
We also need to add property controls to this button for greater usability, flexibility and unplanned changes. The newly added properties would be:
ID: unique identifier in the app to connect business logic
onPress: a function will be triggered on button press
Let’s have a look at how the component will be built:
import react from "react";
import { TouchableOpacity, Text } from "react-native";
export default function Button(props) {
// property declaration
const { title, onPress, primary, secondary, height, width } = props;
return (
<TouchableOpacity onPress={onPress}>
<Text>{title}</Text>
</TouchableOpacity>
);
}
Now, with the default function in place, let’s add styling and different variants.
import React from 'react';
import {TouchableOpacity, Text} from 'react-native';
export default (Button = props => {
// added property declarations
const { title, onPress, secondary, large, height, width, baseColor, textColor } = props;
if (!title) return new Error('No title added!');
// added conditions
const HEIGHT = large ? 60 : height ? height : 40;
const WIDTH = width ? width : 200;
const BACKGROUND_COLOR = secondary ? 'red' : baseColor ? baseColor : 'blue';
const TEXT_COLOR = textColor ? textColor : 'white';
return (
<TouchableOpacity
// added styling
style={{
alignItems: 'center',
justifyContent: 'center',
backgroundColor: BACKGROUND_COLOR,
height: HEIGHT,
width: WIDTH,
}}
onPress={onPress}>
<Text style={{color: TEXT_COLOR}}>{title}</Text>
</TouchableOpacity>
);
});
The above code snippet has added property declarations and conditions. Let’s look at them in detail below:
For height: If
large
is true, then setHEIGHT
to 60. Ifheight
is true, then setHEIGHT
as height, or else setheight
to 40.For width: If
width
is true, then setwidth
as width, or else setwidth
to 200.For
BACKGROUND_COLOR
: Ifsecondary
is true, then setBACKGROUND_COLOR
to red. IfbaseColor
is true, then setBACKGROUND_COLOR
asbaseColor
, or else setBACKGROUND_COLOR
to blue.For
TEXT_COLOR
: IfTEXT_COLOR
is true, then setTEXT_COLOR
astextColor
, or else setTEXT_COLOR
to white.
Set up reusable component in React Native
With the growing number of properties and features in the application, even simple reusable components like Button can become complicated as a result of mutations and possible combinations. Let’s have a look at how we would use this component in our app:
import React from 'react';
import {View, Text, Dimensions, Alert} from 'react-native';
import Button from './src/Button';
const {height, width} = Dimensions.get('screen');
const App = () => {
return (
<View
style={{height, width, alignItems: 'center', justifyContent: 'center'}}>
{/* primary button */}
<Text>Primary</Text>
<Button title="Test Button" />
{/* large primary button */}
<Text>Primary Large</Text>
<Button title="Test Button" large />
{/* secondary button */}
<Text>Secondary</Text>
<Button title="Test Button" secondary />
{/* secondary button */}
<Text>Secondary Large</Text>
<Button title="Test Button" secondary large />
{/* button with custom width and height */}
<Text>custom width and height</Text>
<Button title="Test Button" height={100} width={300} />
{/* button with custom baseColor and custom textColor */}
<Text>Custom colors</Text>
<Button title="Test Button" baseColor="lightpink" textColor="purple" />
{/* button with alert callback function */}
<Text>with onPress callback</Text>
<Button
title="Test Button"
onPress={() => Alert.alert('Button pressed')}
/>
</View>
);
};
export default App;
Execute test with Jest
It’s time to set up the testing configuration for our code. We have already installed all the required dependencies while setting up the application. Create a new file called Button.test.js
, and add the test case as shown below:
import 'react-native';
import React from 'react';
import renderer from 'react-test-renderer';
import Button from './Button';
describe('Testing custom button', () => {
const wrapper = renderer.create(<Button title="Test Button" />);
it('Should render', () => {
expect(wrapper.toJSON()).toBeTruthy();
});
});
At the top of the file, renderer is imported from react-test-renderer
, which provides a container (or wrapper) for our custom component.
describe
marks the start of a new test suite in Jest. The first argument is a String containing a description of what the encompassing test suite is testing. The second argument is a callback function where the test execution steps are written.
it
defines the start of a new test case in Jest. Tests should be as small and concise as they can be, and they should only test one thing. The arguments are the same as those of describe
– a String defining the test scenario and a callback function.
The .toJSON()
and the .toBeTruthy()
assertion function provided by Jest are written inside of the it
block. This will check if the value is not null
or undefined
.
To test the custom reusable Button, the different use cases can be:
Primary button: height 40, width 200, baseColor blue, textColor white
Secondary button: height 40, width 200, baseColor red, textColor white
Size: button can be large or custom width
Custom baseColor: can be applied to both the primary and secondary button
Custom textColor: can be applied to both the primary and secondary button
Below is the test suite for the primary button. For brevity, the test suite of the secondary button is skipped because the methods for the secondary button are the same.
import 'react-native';
import React from 'react';
import renderer from 'react-test-renderer';
import Button from './Button';
describe('Testing primary button', () => {
const wrapper = renderer.create(<Button title="Test Button" />);
console.log(wrapper.toJSON())
const styles = wrapper.toJSON().props.style;
const { height, width, backgroundColor } = styles;
const childStyles = wrapper.toJSON().children[0].props.style;
const {color: buttonTextColor} = childStyles;
it('Should render', () => {
expect(wrapper.toJSON()).toBeTruthy();
});
it('Should have height of 40', () => {
expect(height).toBe(40);
});
it('Should have width of 200', () => {
expect(width).toBe(200);
});
it('Should have blue background', () => {
expect(backgroundColor).toBe('blue');
});
it('Should have white text', () => {
expect(buttonTextColor).toBe('white');
});
});
Let’s say we want to test that our textColor is what we expected. The line console.log(wrapper.toJSON())
gives the output shown below. This is useful for testing our style
and other elements, like children.
{
type: 'View',
props: {
accessible: true,
style: {
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'blue',
height: 40,
width: 200,
opacity: 1
}
...
children: [ { type: 'Text', props: [Object], children: [Array] } ]
}
}
Let’s look at the next example of testing that the textColor
is specified as we expected. From the log above, children
is an array of all the children seen from the root node in the render tree. Moreover, there is only one child in this case, and the styles can be fetched from children with the line of code below:
console.log('Children styles:=>', wrapper.toJSON().children[0].props.style);
children[0]
returns the first object in the array, and props.style
returns the style object. The output would look like this:
Children styles:=> { color: 'white' }
Since the structure becomes clearer with the above output, we can write the declaration and the same type of test case as shown above:
// declaration
const { color: buttonTextColor } = childStyles;
// test case
it('Should have white text', () => {
expect(buttonTextColor).toBe('white');
});
After understanding these techniques, writing tests using Jest and React Native becomes easier for all the other combinations of custom reusable Buttons. Run the test using the command below:
yarn run test
Voila! The tests are written and successfully tested, with all tests passing!
Conclusion
We have covered the basics of custom reusable component (Button) testing for React Native with various approaches. This article should be helpful for writing test cases and test suites for any similar components with multiple properties and combinations. With Jest, testing React Native applications has become easier and more rapid and consistent regardless of the styles declared.
Important links and resources
Here’s an article on end-to-end testing of your React Native apps.
Here’s an article on testing React Native apps with Jest and Codemagic.
Here’s an article on React Native Detox tests on Codemagic.
Here’s an article on Appium mobile testing for React Native.
Here’s an article on why Android developers should pay attention to React Native in 2021.
Here’s an article on choosing the right database for your React Native app.
Here’s an article on how to improve performance of a React Native app.
Here’s an article that describes 9 reasons to use Codemagic CI/CD tool for React Native apps in 2021.
Here’s a practical guide on Continuous Integration and Delivery for React Native apps.
For discussions, learning and support, join the Codemagic Slack Community.
Sneh is a Senior Product Manager based in Baroda. He is a community organizer at Google Developers Group and co-host of NinjaTalks podcast. His passion for building meaningful products inspires him to write blogs, speak at conferences and mentor different talents. You can reach out to him over Twitter (@SnehPandya18) or via email (sneh.pandya1@gmail.com).