How to use Automated Tests on React Native apps
by Rui Fernandes -

Introduction
Automated test development is a powerful technique that provides security to a project, such as the warranty that the code is always working (even after heavy refactoring) and ease of bug tracking.
A practical application of this technique is UI testing. For instance: test if a component was rendered correctly, with the correct properties, colors, actions, events, etc.
So, in this article, I'll show you how to build automated tests on React Native apps. We will implement a small project from scratch and cover these topics:
- Testing children's component rendering;
- Testing component style;
- Testing events/actions (like onPress);
- Testing component properties.
Before reading this article
Make sure you have:
- Notions of automated tests;
- Basic React Native knowledge;
- Basic TypeScript knowledge.
The project
We'll build this project using the Expo Bare Workflow environment, which is a pure React Native environment with Expo modules integration. In addition, we will use TypeScript and Styled Components.
We will create two components:
- UserCardComponent - a card with some user data: image, name, and role.;
- CardContainerComponent - a component that wraps UserCardComponent. Other card components could use it..
The result will be:
Project setup
To create an Expo Bare Workflow project with TypeScript, run this command:
expo init ProjectName --template expo-template-bare-typescript
cd ProjectName
Install the React Native Testing Library:
yarn add -D @testing-library/react-native
Install Styled Components:
yarn add styled-components
yarn add -D @types/styled-components-react-native
Install React Native Responsive Font Size:
yarn add react-native-responsive-fontsize
Use this setting on the tsconfig.json file:
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"jsx": "react",
"strict": true,
"baseUrl": "./",
"allowJs": true,
"strictPropertyInitialization": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*", "src/**/*.json"],
"buildOptions": {},
"compileOnSave": true
}
Add this script on package.json:
"test": "jest"
Hands on
CardContainerComponent
Create the src/components/card-container folder inside the project root. And then create the following files:
- index.tsx: A React Component
import React from 'react';
import { TouchableOpacityProps } from 'react-native';
import { Container } from './styles';
export const CardContainerComponent: React.FC<TouchableOpacityProps> = ({
children, ...rest
}) => (
<Container testID="card-container-component" {...rest}>{children}</Container>
);
- styles.ts: Component styles
import { TouchableOpacity } from 'react-native';
import { RFValue } from 'react-native-responsive-fontsize';
import styled from 'styled-components/native';
export const Container = styled(TouchableOpacity)`
flex-direction: row;
align-items: center;
background-color: #eee;
padding: ${RFValue(8)}px ${RFValue(12)}px;
border-radius: ${RFValue(8)}px;
`;
- card-container.spec.tsx: File with the automated tests (currently empty)
After that, the project structure will be like this:
(...)
node_modules
src
├── components
│ ├── card-container
│ │ ├── card-container.spec.tsx
│ │ ├── index.tsx
│ │ └── styles.ts
(...)
Test coverage
To check the code test coverage, use this command:
yarn test --coverage --passWithNoTests
This command will generate a coverage folder on the project root:
.
├── clover.xml
├── coverage-final.json
├── lcov-report
│ ├── App.tsx.html
│ ├── base.css
│ ├── block-navigation.js
│ ├── card-container
│ │ ├── index.html
│ │ ├── index.tsx.html
│ │ └── styles.ts.html
│ ├── favicon.png
│ ├── index.html
│ ├── index.tsx.html
│ ├── prettify.css
│ ├── prettify.js
│ ├── sort-arrow-sprite.png
│ ├── sorter.js
│ ├── styles.ts.html
│ └── user-card
│ ├── index.html
│ ├── index.tsx.html
│ ├── props.ts.html
│ ├── styles.ts.html
│ └── user-card.specasd.tsx.html
└── lcov.info
Now, open /coverage/lcov-report/index.html.
The result will be this:
The first test
First of all, let's render the component using the render method of Testing Library.
Open the card-container.spec.tsx file and code this:
import React from 'react';
import { render } from '@testing-library/react-native';
import { View } from 'react-native';
import { CardContainerComponent } from '.';
// Describe: a test set
describe('CardContainerComponent', () => {
// Test
test('Should render with a children', () => {
render(
<CardContainerComponent>
</CardContainerComponent>,
);
});
});
Testing child component rendering
The CardContainerComponent must wrap a card component, getting it as a children. In that case, how to test if the children will be rendered or not?
So, create a new test (inside describe), calling the render method and passing CardContainerComponent as a parameter, as we did in the last test.
Now, we'll pass a View with a testID property as a children. This testID property helps us search for the component inside the test.
test('Should render with a children', () => {
render(
<CardContainerComponent>
<View testID="card-container-children" />
</CardContainerComponent>,
);
});
The render method of Testing Library returns an object with several helpful methods to build automated tests. One of them is the getByTestId method.
test('Should render with a children', () => {
const { getByTestId } = render(
<CardContainerComponent>
<View testID="card-container-children" />
</CardContainerComponent>,
);
});
The getByTestId method searches for a component that has the given testID. It also has the following behavior:
- If it finds the component, then returns an object of the component;
- Else, it throws an exception.
In that case, we need to check only if the children component was rendered or not.
So, create a function that calls getByTestId and expect that it won't throw any exception:
expect(() => getByTestId('card-container-children')).not.toThrow();
After that, the test will be like this:
describe('CardContainerComponent', () => {
test('Should render with a children', () => {
const { getByTestId } = render(
<CardContainerComponent>
<View testID="card-container-children" />
</CardContainerComponent>,
);
expect(() => getByTestId('card-container-children')).not.toThrow();
});
});
Testing component style
We will now create a new test, in which we will check if the component style was successfully updated.
In that case, we'll do these steps:
- Pass a custom border radius to the CardContainerComponent;
- Call getByTestId to search the component;
- Check if the borderRadius was successfully updated.
The test will be like this:
test('Should render with a custom border radius', () => {
const customBorderRadius = 50;
// First step
const { getByTestId } = render(
<CardContainerComponent style={{ borderRadius: customBorderRadius }} />,
);
// Second step
const container = getByTestId('card-container-component');
// Third step
expect(container.props.style.borderRadius).toBe(customBorderRadius);
});
Testing actions (onPress)
A simple way to build this kind of test is creating a wasClicked variable, that starts as false and, when clicking the component, the value changes to true.
After that, we check if wasClicked is truthy.
So, the Testing Library has a fireEvent method, which can simulate mobile app events and actions.
In our case, we want to test the press event:
test('Should render with an action', () => {
// Initial variable state
let wasClicked = false;
// Render card container with a onPress event
const { getByTestId } = render(
<CardContainerComponent onPress={() => { wasClicked = true; }} />,
);
// Search for the component
const container = getByTestId('card-container-component');
// Simulate press event
fireEvent.press(container);
expect(wasClicked).toBeTruthy();
});
Coverage after adding the tests
UserCardComponent
Now, let's create the UserCardComponent, which will receive an image, a name, and a role.
The folder structure will be very similar to the CardContainerComponent, but we'll add a new file (props.ts):
- index.tsx: The React component
import React from 'react';
import { CardContainerComponent } from '../card-container';
import { UserCardComponentProps } from './props';
import {
DataContainer, Name, ProfileImage, Role,
} from './styles';
export const UserCardComponent: React.FC<UserCardComponentProps> = ({
userImage,
userName,
userRole,
...rest
}) => (
<CardContainerComponent {...rest}>
<ProfileImage source={userImage} testID="profile-image" />
<DataContainer>
<Name>{userName}</Name>
<Role>{userRole || 'Developer'}</Role>
</DataContainer>
</CardContainerComponent>
);
- styles.ts: Component styles
import { RFValue } from 'react-native-responsive-fontsize';
import styled from 'styled-components/native';
export const ProfileImage = styled.Image`
width: ${RFValue(70)}px;
height: ${RFValue(70)}px;
border-radius: ${RFValue(70)}px;
`;
export const DataContainer = styled.View``;
export const Name = styled.Text`
font-size: ${RFValue(16)}px;
font-weight: 700;
margin-left: ${RFValue(8)}px;
`;
export const Role = styled.Text`
font-size: ${RFValue(16)}px;
font-weight: 400;
margin-left: ${RFValue(8)}px;
`;
- props.ts: Component types
import { ImageSourcePropType, TouchableOpacityProps } from 'react-native';
export interface UserCardComponentProps extends TouchableOpacityProps {
userImage: ImageSourcePropType;
userName: string;
userRole?: string;
}
- card-container.spec.tsx: File with the automated tests (currently empty)
After that, the project structure will be like this:
(...)
node_modules
src
├── components
│ ├── card-container
│ │ ├── card-container.spec.tsx
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── user-card
│ │ ├── index.tsx
│ │ ├── props.ts
│ │ ├── styles.ts
│ │ └── user-card.spec.tsx
(...)
Current test coverage
Testing component properties
We will divide this test into three steps:
- Search for the rendered image and check if its uri is the same as the uri we passed;
- Check if the name we passed was rendered in the component;
- Check if the role we passed was rendered in the component;
In the first step, we'll use the getByTestIdMethod.
In the second and the third, we'll use the getByTextMethod, which searches for some text inside the rendered component. If it doesn't find that text, it throws an exception.
The test will be like this (user-card.spec.tsx):
import React from 'react';
import { render } from '@testing-library/react-native';
import { UserCardComponent } from '.';
describe('UserCardComponent', () => {
test('Should render correctly', () => {
const imageUri = 'https://github.com/ruifernandees.png';
const { getByText, getByTestId } = render(
<UserCardComponent
userImage={{ uri: imageUri }}
userName="Rui Fernandes"
userRole="Developer"
/>,
);
// First step
const image = getByTestId('profile-image');
expect(image.props.source.uri).toBe(imageUri);
// Second step
expect(() => getByText('Rui Fernandes')).not.toThrow();
// Third step
expect(() => getByText('Developer')).not.toThrow();
});
});
Coverage after the last test
Notice that even when testing 100% of the component's statements, there are still some untested branches, as we'll see in the figure.
But what are these branches? They are untested cases that the code has. Usually, in React components this happens when we have conditional rendering.
To solve this, let's check where we need to test. Click on the user-card link.
Notice that the file that doesn't have 100% of branch coverage is the index.tsx. So, click on its link:
In yellow, the coverage report shows us the untested branch.
The not tested case is when "userRole" is not defined, and the component renders a default value:
To solve that problem, let's create a test that we don't pass userRole:
test('Should render with default user role', () => {
const imageUri = 'https://github.com/ruifernandees.png';
const { getByText, getByTestId } = render(
<UserCardComponent
userImage={{ uri: imageUri }}
userName="Rui Fernandes"
/>,
);
const image = getByTestId('profile-image');
expect(image.props.source.uri).toBe(imageUri);
expect(() => getByText('Rui Fernandes')).not.toThrow();
});
Final Coverage
Now the code coverage is 100%.
Presenting the components
We finished the components implementation. Now let's show them in the app.
Create a home holder inside src/pages, with the following files:
- index.tsx: React component
import React from 'react';
import { UserCardComponent } from '../../components/user-card';
import { Container, Title } from './styles';
export const HomeScreen: React.FC = () => (
<Container>
<Title>UserCardComponent Example:</Title>
<UserCardComponent
userImage={{ uri: 'https://github.com/ruifernandees.png' }}
userName="Rui Fernandes"
userRole="Developer"
/>
</Container>
);
- styles.ts: Component styles
import { RFValue } from 'react-native-responsive-fontsize';
import styled from 'styled-components/native';
export const Container = styled.View`
flex: 1;
background-color: #fff;
padding: 0 ${RFValue(16)}px;
justify-content: center;
`;
export const Title = styled.Text`
text-align: center;
font-size: ${RFValue(20)}px;
font-weight: 500;
margin-left: ${RFValue(8)}px;
margin-bottom: ${RFValue(4)}px;
`;
Move the App.tsx file from the root folder to the src folder and code this:
import React from 'react';
import { StatusBar } from 'react-native';
import { HomeScreen } from './pages/home';
export function App() {
StatusBar.setBarStyle('dark-content', true);
return <HomeScreen />;
}
Update the App.tsx file importation inside index.js:
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { App } from './src/App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
Start metro:
yarn start
Run the app:
# Android
yarn android
# iOS
yarn ios
Conclusion
Congrats! Now you learned how to test React Native applications in a professional way. You made in this article tests related to events, styles, properties, and rendering. Also, you learned how to increase your test coverage.
If you want to see the complete source code of the project, click here to access the Github repository.
Thanks for reading!