Rui Fernandes

How to use Automated Tests on React Native apps

by Rui Fernandes -

Imagem de cover

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:

Before reading this article

Make sure you have:

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:

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:

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>
);
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;
`;

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:

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:

  1. Pass a custom border radius to the CardContainerComponent;
  2. Call getByTestId to search the component;
  3. 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):

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>
);
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;
`;
import { ImageSourcePropType, TouchableOpacityProps } from 'react-native';

export interface UserCardComponentProps extends TouchableOpacityProps {
  userImage: ImageSourcePropType;
  userName: string;
  userRole?: string;
}

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:

  1. Search for the rendered image and check if its uri is the same as the uri we passed;
  2. Check if the name we passed was rendered in the component;
  3. 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:

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>
);
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!

Rui Fernandes

Rui Fernandes

Focused Full Stack Engineer creating impactful digital solutions.