Detox for your React Native App

E2E UI Testing explained— Recap of the MD Tech Meetup at UniNow

Philipp Ucke
UniNow Engineering
5 min readSep 30, 2022

--

https://wix.github.io/Detox/

Introduction

When it comes to building apps, software testing is critical. It assures the quality of the product and satisfies the customers as well as the users. In the web development world, end-to-end testing is a widely adopted technique and because of tools like Cypress, Puppeteer and Playwright it is also easy to do. But when it comes to mobile applications, end-to-end testing is not so common. My guess is that many app developers are discouraged by the setup and configuration needed to add end-to-end testing to their apps.

Detox is a gray box end-to-end testing and automation framework for react native apps. It benefits from JavaScript test files, similar to those Jest uses, that run on both, iOS and Android. And due to its gray boxiness, Detox monitors asynchronous operations in your app and synchronizes them to prevent flakiness. We will show how to set up detox in a react native application and write some basic tests.

For the sake of time, we are limiting ourselves to iOS. Let us know if you want a dedicated walkthrough for Android as well.

Set up detox for your react native application

We assume that you already completed the setup for:

  • macOS Catalina or newer
  • Xcode (≥ v12.x)
  • Xcode command-line tools (xcode-select -install)
  • Homebrew
  • Node (≥ v12.0)
  • react-native application you can start with

At first, you have to prepare the environment of your react native app. Start by installing the detox command line tools. This package makes it easier to operate detox from the command line and enables usage outside your npm scripts.

npm install detox-cli --global
# or
yarn global add detox-cli

Detox uses a collection of utilities to query and communicate with the simulator, to install them and run the following commands.

brew tap wix/brew
brew install applesimutils

Set up a test runner

Detox delegates the actual JavaScript test-code execution to a dedicated test-runner. It supports the popular Jest and Mocha out of the box. We choose Jest for now. To add Jest to your project run

npm install -D jest@>=27.2.5

Detox provides a configuration setup script that creates all the necessary files for Jest and Detox itself.

detox init -r jest

This should create the following files:

  • an e2e/ folder in your root
  • an e2e/config.json file
  • an e2e/environment.js file
  • an e2e/firstTest.e2e.js file

Make sure the e2e/config.js file looks like this

{
"maxWorkers": 1,
"testEnvironment": "./environment",
"testRunner": "jest-circus/runner",
"testTimeout": 120000,
"testRegex": "\\.e2e\\.js$",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}

And the e2e/environment.js file looks like this

const {
DetoxCircusEnvironment,
SpecReporter,
WorkerAssignReporter,
} = require('detox/runners/jest-circus');

class CustomDetoxEnvironment extends DetoxCircusEnvironment {
constructor(config, context) {
super(config, context);

this.initTimeout = 300000;

// This takes care of generating status logs on a per-spec
// basis. By default, Jest only reports at file-level.
// This is strictly optional.
this.registerListeners({
SpecReporter,
WorkerAssignReporter,
});
}
}

module.exports = CustomDetoxEnvironment;

For more information on how to configure Jest with Detox visit the official documentation. (https://wix.github.io/Detox/docs/guide/jest/)

Configure Detox

Now that our environment is ready, there is only one thing left to do. We have to configure detox itself. The general configuration takes place in a /.detoxrc.json file that is located in your root directory. You have to specify the following fields:

  • testRunner specifies which test runner you are using
  • runnerConfig specifies the path to your config.json
  • apps can hold multiple app configs
  • devices can hold multiple device configs
  • configurations maps apps to devices

You can use the example configuration below to get started.

{
"testRunner": "jest",
"runnerConfig": "e2e/config.json",
"apps": {
"ios": {
"name": "YourAppName",
"type": "ios.app",
"binaryPath":
"ios/build/Products/Debug-iphonesimulator/YourAppName.app",
"build":
"xcodebuild -workspace ios/YourAppName.xcworkspace -scheme YourAppName -sdk iphonesimulator -derivedDataPath ios/build"
}
},
"devices": {
"simulator": {
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
}
}
},
"configurations": {
"ios": {
"device": "simulator",
"app": "ios"
}
}
}

You can get further guidance and clarity from the official docs.

Now everything is set up and you can start by building your app using the detox-cli. Detox executes the build command from your config, so make sure to provide the correct one.

detox build --configuration <your configuration name>

If you are building a debug version of your app, make sure to start the metro bundler beforehand

Write some tests

While detox is offering many ways to find a specific component on your screen (take a look at all the matchers), the best and clearest way is through testID 's. Almost every react-native component can be equipped with a testID prop, to give it a unique id in your application. (Make sure that the testID you specify is really unique)

<TouchableOpacity 
onPress={handleNextButton}
testID={`${testID}.next.button`}
>
<Text>Next</Text>
</TouchableOpacity>

Once your application or screen is decorated with testID 's, you can start with writing the real test.

A detox test looks very similar to a general JavaScript test. At the top level, there is a describe block, that groups tests of the same flow/screen together. You can define lifecycle hooks inside the describe block to execute certain actions, like reloading the application to start from a fresh app state. And then there are the it blocks that contain a composition of actions, matchers, and expectations. The test structure could look like this:

  1. Validate the starting point
  2. Perform action
  3. Expect a certain outcome

And as code:

describe("Onboarding Screen", () => {
beforeAll(async () => {
await device.launchApp({
newInstance: false,
permissions: { location: "always", notifications: "YES" },
});
});

beforeEach(async () => {
await device.reloadReactNative();
});

it("should swipe through onboarding pages", async () => {
// Validate starting position
await expect(element(by.id("onboarding.screen"))).toBeVisible();
await expect(element(by.id("list"))).toBeVisible();
await expect(element(by.id("list.0.item"))).toBeVisible();
// Perform action
await element(by.id("list")).swipe("left");
// Expect certain outcome
await expect(element(by.id("list.0.item"))).not.toBeVisible();
await expect(element(by.id("list.1.item"))).toBeVisible();
});
});

Now it's time to finally run the test and watch detox controlling your device.

detox test --configuration <your configuration name>
E2E Testing the UniNowten App

Conclusion

Congratulations! 🎊
You successfully prepared your environment, installed and configured detox, and wrote your first test for your react-native application. The first stone on the way to a fully tested app is now set.

After writing this article we have to admit, setting up an end-to-end testing framework is definitely not trivial. And further improvement is needed in this area. Nevertheless, we hope that this article is able to simplify and speed up setting up end-to-end testing for your react-native applications.

Thanks for reading!

We hope you learned something new about detox and end-to-end testing in react native applications. If there are any questions, comment down below, we would love to answer them.

If you want to hear us live — take a look at the upcoming events.
https://uninow.io/

--

--