Robust React Native App — 1

Abdullah Süha ışık
10 min readJul 6, 2023

Nowadays, building apps has become relatively easier, thanks to the abundance of tutorials and boilerplate code available on the internet. It has opened up opportunities for individuals to create their own apps with relative ease. However, the real challenge lies in developing a robust app with a solid structure.

In this article series, I will delve into the topic of building a robust React Native app that is fortified with unit tests for its components, as well as integration and end-to-end (E2E) tests. By implementing a comprehensive testing strategy, we can ensure that our app functions reliably and as intended, even as it grows in complexity and scale. I am going to write a series related and planning 5 articles about it. The topics are :

1. Project Overview and the Importance of Testing

Before we dive into the details, let’s start with a brief project overview. We’ll explore the significance of testing in the React Native app development process and why it is crucial for creating a successful and robust application.

2. Writing Unit Tests

In this section, we will explore how to write effective unit tests that isolate and verify the functionality of each component. By testing individual units of code in isolation, we can catch potential issues early on and maintain the desired behavior of our app.

3. Writing E2E Test with Detox

In addition to unit tests, e2e tests are essential for verifying the proper interaction between various components of your React Native app. We will explore the usage of Detox, a powerful end-to-end testing framework, to write e2e tests that simulate user interactions and validate the behavior of our app as a whole. These tests will ensure that the different parts of our app work harmoniously together, providing a seamless user experience.

4. Fastlane CI/CD process

Continuous Integration and Continuous Deployment (CI/CD) are vital practices in modern app development workflows. In this section, we will discuss how to integrate a fastlane CI/CD process into your React Native app. By automating the build, test, and deployment processes, we can streamline development and ensure that changes are thoroughly tested and deployed efficiently.

5. Streamlining Processes with Oclif

Repetitive tasks can consume valuable development time and effort. In this final section, we will explore how to leverage Oclif, a command-line framework, to simplify and automate repetitive processes in your React Native app development (clean and setup process). By creating custom commands and automating tasks, you can enhance productivity and focus on building high-quality features for your app.

Here is my project. I am going to create a branch for every article. You can start investigating with the master branch. After cloning the app, you can do your own, following me.

If you watch a Youtube video, you could see all features and available screens. I wanted to make this article like a real-life example. The project’s structure is the small version that we used on our projects. I read lots of article-related Unit tests and E2E tests, but they focus on theory in this series I am going to touch on a real example, and we will be coding the tests together. Before diving into the tests, let me introduce my app and a little bit of talk about tests.

Let’s consider the scenario where we are creating an English reading habit app, our goal is to help clients get reading habits. We are going to make robust authentication for this app. The authentication features have already been implemented. We are not going to develop the app’s features in this article, we are going to just focus on tests. Before going to test-related topics, please take a careful look at the project’s master branch.

The app consists of five screens: Onboarding, Login, SignIn, ForgetPassword, and HomeScreen.

Followed libraries were used react-native-navigation, react-native-svg, and redux-saga and the app was written with typescript. In this tutorial, you can also find redux-saga, typescript usages and examples, and react-navigation typescript examples.

File Structure

What is the Unit Test, and why should I write it?

The purpose of the unit test is validating your individual components and functions (unit) with code, is providing and checking your code to ensure it is working as expected. Unit testing is the simplest and least expensive testing option. Unit testing helps the React component conform to standards. I’ve noticed that most of the time when I’m writing a unit test, I’m increasing the quality of the component.

Another benefit of unit testing is to help to understand the code that is written by another developer. Indeed, if your component doesn’t follow some of the unwritten rules and standards you can’t test reliablely the components, and you have to refactor your components.

What is the Integration Test?

Integration test is just more complex unit test. If you want to test your component's interaction with others. For example, if your component makes async operations, and you want to test it with the result of the async operation, it calls an integration test, however, you have to mock this async operation. Another example is when you manage state using Redux and want to test a component that uses Redux; this is called integration testing.

What is the E2E test?

E2E testing, also known as Gray Box Testing, allows you to test your entire system from a user’s perspective. The focus is on whether the application works, rather than how it works. I highly recommend E2E testing as it helps avoid time wastage. You may wonder how it helps to avoids wasting time, considering that writing E2E tests can be time-consuming. We will delve into this topic in the related article, but I can assure you that during development, developers often perform numerous manual tests. When adding a new feature, we test it multiple times, potentially breaking things, and wasting time on manual tests. By automating these tests, we can save valuable time.

Summary:

Unit tests focus on testing individual components or functions in isolation, whereas E2E (end-to-end) tests verify the integrated behavior of the application as a whole. Unit tests provide quick feedback and help identify bugs within specific code units, while E2E tests validate the end-to-end functionality of the system. Both types of tests are valuable and serve complementary purposes in ensuring the quality and reliability of a software application. Unit tests help ensure that individual units of code work correctly, while E2E tests ensure that all the different parts of the application work together seamlessly. By combining both types of tests, developers can have confidence in the stability and correctness of their software.

Before diving into the unit test topic, let’s familiarize ourselves with some terms.

Jest is the most popular JS test framework.

What is the Test Runner?

A test runner is responsible for executing tests. It locates test files, provides an execution environment, and delivers results. We will be using Jest, which is a widely used and popular test runner. Other JavaScript test runners include Enzyme, Jasmine, and more.

What are Assertions?

When writing tests, assertions are used to compare the actual value with the expected value. Jest provides a range of matchers for this purpose. These matchers allow us to validate different values using different comparison techniques and modifiers.

Here are several matchers’ documents by JEST;

React-Native initially relies on the react-test-renderer library, which is built upon Jest, for testing purposes. However, we have decided to utilize alternative packages instead of the react-test-renderer library for our testing needs. They are @testing-library/jest-native and @testing-library/react-native

@testing-library/jest-native

That library provides custom element matchers for jest. These matchers are;

They provide enhanced testing capabilities specifically tailored for React Native elements.

@testing-library/react-native

Basically, this library provides rendering components and grap rendered elements more easily and elegant way than react-test-render.

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

Let’s start coding

First, clone the app and install the dependencies

git clone git@github.com:abdullahsuhaisk/robustReactNative.git

npm install (install node_modules dependencies)

npx pod-install (install iOS dependencies)

If everything succeeds, you will be able to run iOS and android

npm run ios

If you see the onboarding screen, everything is working as expected.

Let’s run npm run test

If you encounter an error, don’t worry. Jest found our test files in the __test__ folder and executed them. Let's remove the /root/__test__ folder as we won't be needing it.

Adding Test Library

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

After adding the dependencies, we need to make some configurations. Update your package.json file as follows:

{
"name": "buildingRobustReactNativeApp",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
"@react-native-community/checkbox": "^0.5.15",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@redux-devtools/extension": "^3.2.5",
"axios": "^1.4.0",
"react": "18.2.0",
"react-native": "0.71.11",
"react-native-safe-area-context": "^4.6.3",
"react-native-screens": "^3.21.1",
"react-native-svg": "^13.9.0",
"react-native-svg-transformer": "^1.0.0",
"react-redux": "^8.1.0",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-saga": "^1.2.3"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native-community/eslint-config": "^3.2.0",
"@testing-library/jest-native": "^5.4.2",
"@testing-library/react-native": "^12.1.2",
"@tsconfig/react-native": "^2.0.2",
"@types/jest": "^29.2.1",
"@types/react": "^18.0.24",
"@types/react-test-renderer": "^18.0.0",
"@types/redux-logger": "^3.0.9",
"babel-jest": "^29.2.1",
"eslint": "^8.19.0",
"jest": "^29.2.1",
"metro-react-native-babel-preset": "0.73.10",
"prettier": "^2.4.1",
"react-test-renderer": "18.2.0",
"typescript": "4.8.4"
},
"jest": {
"preset": "react-native"
}
}

Remove the "jest" key from the package.json file and create a new file called jest.config.json in the root directory with the following content:

{
"preset": "react-native",
"setupFilesAfterEnv": ["<rootDir>/src/tests/setup.ts"],
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation)"
]
}

Navigate to the src directory and create a tests folder inside it. Inside the tests folder, create a file named setup.ts. For now, you can add the following line at the top of the file to enable the extended expected functionality:

import '@testing-library/jest-native/extend-expect'

After we are going to add global mock functions into the setup.ts

Let’s check our new test environments work correctly.

Create a new file named jest-native.test.tsx under the src/tests. Because we want to check new matchers, and renderer work correctly.

import * as React from 'react';
import { StyleSheet, View, Button, Text, TextInput } from 'react-native';
import { render } from '@testing-library/react-native';

// import '@testing-library/jest-native/extend-expect';

const style = StyleSheet.create({
style1: {
color: 'red',
backgroundColor: 'green',
},
});

test('jest-native matchers work correctly', () => {
const { getByText, getByA11yHint } = render(
<View>
<Button title="Enabled Button" onPress={jest.fn()} />
<Button title="Disabled Button" disabled={true} onPress={jest.fn()} />
<Text accessibilityHint="Empty Text" style={style.style1} />
<Text accessibilityHint="Not Empty Text">Not empty</Text>
<View accessibilityHint="Empty View" />
<View accessibilityHint="Not Empty View">
<Text />
</View>
<View accessibilityHint="Container View">
<View accessibilityHint="First-Level Child">
<Text>Second-Level Child</Text>
</View>
</View>
<TextInput
accessibilityHint="Text Input"
allowFontScaling={false}
secureTextEntry={true}
defaultValue="111"
/>
</View>,
);

expect(getByText('Enabled Button')).toBeEnabled();
expect(getByText('Disabled Button')).not.toBeEnabled();
expect(getByText('Disabled Button')).toBeDisabled();
expect(getByText('Enabled Button')).not.toBeDisabled();

expect(getByA11yHint('Empty Text')).toBeEmptyElement();
expect(getByA11yHint('Empty View')).toBeEmptyElement();
expect(getByA11yHint('Not Empty Text')).not.toBeEmptyElement();
expect(getByA11yHint('Not Empty View')).not.toBeEmptyElement();

expect(getByA11yHint('Container View')).toContainElement(
getByA11yHint('First-Level Child'),
);
expect(getByA11yHint('Container View')).toContainElement(
getByText('Second-Level Child'),
);
expect(getByA11yHint('Container View')).not.toContainElement(
getByText('Enabled Button'),
);

expect(getByA11yHint('Not Empty Text')).toHaveTextContent('Not empty');
expect(getByA11yHint('Not Empty Text')).toHaveTextContent(/Not empty/);
expect(getByA11yHint('Not Empty Text')).not.toHaveTextContent('Is empty');

expect(getByA11yHint('Empty Text')).toHaveStyle({ color: 'red' });
expect(getByA11yHint('Empty Text')).toHaveStyle({
color: 'red',
backgroundColor: 'green',
});
expect(getByA11yHint('Empty Text')).not.toHaveStyle({ color: 'green' });
expect(getByA11yHint('Empty Text')).not.toHaveStyle({
color: 'green',
backgroundColor: 'green',
});

const textInput = getByA11yHint('Text Input');
expect(textInput).toBeTruthy();
expect(textInput).toHaveProp('allowFontScaling');
expect(textInput).toHaveProp('allowFontScaling', false);
expect(textInput).toHaveProp('secureTextEntry');
expect(textInput).toHaveProp('secureTextEntry', true);
expect(textInput).toHaveProp('defaultValue');
expect(textInput).toHaveProp('defaultValue', '111');
});

Then run npm run test

Voilà, everything works correctly.

If you are stuck, you can get help at https://github.com/abdullahsuhaisk/robustReactNativeApp/commit/d30dcf4b88f71a6629f92c9d3740faa7183f8059 and unitTest branch is related unit Test, also you can inspect that.

Code Coverage

Coverage is defined asthe reporting of a particular important event or subject on Cambridge Dictionary.

Coverage helps me to visualise what did I tested, what should I test.

npm test -- --coverage

It only worked on one test file. We need to introduce our components to coverage actions.

npm run test -- --coverage --watchAll --collectCoverageFrom='src/components/**/*{ts,tsx}'

We introduced which folder it should cover. It generates a folder in root, that calls coverage.

Most time Uncovered Line will help you to target what didn’t I test.

Open coverage/Icov-report/index.html

The coverage command also creates reports HTML page through jest. Add coverage command into packages JSON.

 "scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"debug": "open 'rndebugger://set-debugger-loc?host=localhost&port=8081'",
"coverage": "npm run test -- --coverage --watchAll --collectCoverageFrom='src/components/**/*.{ts,tsx}' --collectCoverageFrom='!src/**/*.{types, stories, constants, test, spec}.{ts,tsx}'"
},

Now you can only run npm run coverage

Next article, I am going to write each component's unit test.

For more detail
https://www.npmjs.com/package/@testing-library/react-native

https://testing-library.com/docs/ecosystem-jest-native/

--

--