Set up Detox for end-to-end testing in your React Native App

Recently some of us have been working on a React Native project to improve our skills and learn more about mobile development. The tech background for the majority of us is Ruby and Ruby on Rails so you can imagine that we love to TDD the features we create.

We want to bring this practice to our React Native project and we found Detox as a great tool to create end-to-end tests. When using Detox with Expo and TypeScript, a lot of different configurations and steps are required. The documentation exists for each tool but is scattered over the internet. We decided to write this guide to help others that are in the same situation as us.

Pre-requisites

In this post we will assume that you have setup an Expo React Native project with TypeScript and Jest for the testing library.

A few tools are required to have a fully working testing environment. While they are not always directly linked with the purpose of this article, we found that some steps were suffering from a lack of documentation on the internet. We are going to list some important requirements for the sake of helping you have everything set up, but please feel free to jump to the next section if you want to start with Detox immediately.

Java is required to run the tests on Android and applesimutils is needed to run the tests on iOS.

Java

You can install Java using Homebrew:

brew install java

Make sure Java is properly installed and available in your terminal using java -version. You should see a result like:

openjdk version "21.0.2" 2024-01-16
OpenJDK Runtime Environment Homebrew (build 21.0.2)
OpenJDK 64-Bit Server VM Homebrew (build 21.0.2, mixed mode, sharing)

Otherwise, one workaround is to create a symlink to the Java installation:

sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk \
     /Library/Java/JavaVirtualMachines/openjdk.jdk

applesimutils

You can install applesimutils using Homebrew:

brew tap wix/brew
brew install applesimutils

Path to node_modules binaries

As a final note, you should have access to the bin directory from node_modules in your path.

# .zshrc or .bashrc

export PATH=$PATH:./node_modules/.bin

What is Detox and why should I use it?

Detox is a testing library for React Native that allows us to write end-to-end tests for both iOS and Android applications. Thanks to Detox we can write end to end tests of our features in a way that simulates the user interactions with the application.

In the documentation of Detox we can find the following features that make it a great tool for testing React Native:

  • Cross Platform: Write end-to-end tests in JavaScript for React Native apps (Android & iOS).
  • Debuggable: Modern async-await API allows breakpoints in asynchronous tests to work as expected.
  • Automatically Synchronized: Stops flakiness at the core by monitoring asynchronous operations in your app.
  • Made For CI: Execute your E2E tests on CI platforms like Travis CI, Circle CI or Jenkins without grief.
  • Runs on Devices: Gain confidence to ship by testing your app on a device/simulator just like a real user (not yet supported on iOS).
  • Test Runner Agnostic: Detox provides a set of APIs to use with any test runner without it. It comes with Jest integration out of the box.

Let’s start! Adding Detox to our project

First we need to install Detox in our project as a development dependency. Yarn is our default utility but the commands in this article can be performed with other tools like NPM, NPX and others.

yarn add --dev detox
yarn add --dev @config-plugins/detox

We then need to add or update the "plugins" key in app.json, to include the detox plugin to add support for Android.

{
  "expo": {
    "plugins": ["@config-plugins/detox"]
  }
}

Prebuild

Expo provides a prebuildcommand to generate native code for a React Native project. This step is necessary for us as it will create the package/bundle structure and configuration that Detox depends on.

yarn expo prebuild

We will be prompted with a few questions related to the name of the package.

Configure Detox with Jest

If you don’t have Jest declared as a development dependency, you can add it with the following method.

yarn add --dev jest

Detox provides an init command that supports different testing environments, which is Jest in our case. It generates a new e2e folder where the Jest configuration and tests resides and .detoxrc.js file at your project root to configure Detox.

yarn detox init -r jest

By default, the command will create a starter test that you can keep, change or delete. We will add our own test later in this article.

.detoxrc.js configures the simulators/emulators used during the tests, and the name of the packages. Feel free to check the devices key and ensure Detox will use the devices you intend to.

You will find the value YOUR_APP multiple times in the file. This value needs to be updated with the name of your project. We named ours Pokedex. You might want to check the name created in the ios/ folder, generated by theexpo prebuild command, if you are ensure about the name of the format for your application.

module.exports = {
  // ...
  devices: {
    simulator: {
      type: "ios.simulator",
      device: {
        type: "iPhone 15",
      },
    },
    emulator: {
      type: "android.emulator",
      device: {
        avdName: "pixel_4",
      },
    },
  },
};

Configuring Jest with TypeScript

While this step is not necessary, we recommend using TypeScript in your
project and in your tests. This will add consistency, confidence and efficiency to your testing process. Let’s first add the dependencies.

yarn add --dev ts-jest @types/jest

We also need to change the supported test file extensions in e2e/jest.config.js, to support JavaScript and TypeScript files:

module.exports = {
  // ....
  testMatch: ["<rootDir>/e2e/**/*.test.[jt]s?(x)"],
};

Adding our first test

At this point, all the configuration should be over, but we still need to create a brand new test and generate the packages for Detox to run the tests. Let’s add a new test in the e2e/ directory:

// e2e/App.test.ts

import { expect, by, device, element } from "detox";

describe("Home screen", () => {
  beforeAll(async () => {
    await device.launchApp();
  });

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

  it("shows a visible button", async () => {
    await expect(element(by.text("Open Pokedex"))).toBeVisible();
  });

  it("shows the surprise after tapping the button", async () => {
    await element(by.text("Open Pokedex")).tap();
    await expect(element(by.text("Gotta Catch ’Em All"))).toBeVisible();
  });
});

Building our debug iOS and Android apps

We are getting close to running our first end-to-end test! But first we need to build the debug versions of our iOS and Android apps. The Apple platform needs an extra step to install all the dependencies.

cd ios
pod install
cd ...

We can now build the apps:

detox build --configuration ios.sim.debug
detox build --configuration android.emu.debug

Running the test

Finally we can run the test! First, make sure the iPhone Simulator and the Android Emulator are up and running.

Then, each of the following commands will run our end-to-end test on the appropriate platform.

detox test --configuration ios.sim.debug
detox test --configuration android.emu.debug

Now is the time to celebrate our first big victory: A failing test suite! 🤡

The failing test should look like this:

Picture of a failing test

This is actually a very important goal we just reached: Our testing environment is now configured and working and we are already engaged in our TDD process. Now let’s make this test pass.

Let’s add our business code

Here is one of the simplest implementations we can produce to satisfy the test’s requirements.

// App.tsx

import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

export default function App() {
  const [clicked, setClicked] = useState(false);

  return (
    <View style={styles.container}>
      {!clicked && (
        <Pressable style={styles.button} onPress={() => setClicked(true)}>
          <Text style={styles.text}>Open Pokedex</Text>
        </Pressable>
      )}
      {clicked && <Text style={styles.surprise}>Gotta Catch ’Em All</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  text: {
    fontSize: 16,
    fontWeight: "bold",
    color: "white",
  },
  surprise: {
    fontSize: 30,
    color: "#ff0000",
  },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 32,
    backgroundColor: "#ff0000",
  },
});

Now, if all the configuration went well, running the test commands again should show you a passing test suite.

Picture of a passing test

Congratulations! 🎉

Adding new scripts to our package.json

To simplify the introduction of end-to-end tests in your testing process, we suggest to add a simple custom command in package.json which runs Detox on both platforms at the same time.

{
  "test:detox:android": "detox test --configuration android.emu.debug",
  "test:detox:ios": "detox test --configuration ios.sim.debug",
  "test:detox": "yarn test:detox:ios && yarn test:detox:android"
}

Now we can run the tests with a single command:

yarn test:detox

Conclusion

We have seen how to setup Detox in a React Native project with TypeScript and Jest. We have also seen how to configure Jest with TypeScript and how to build the debug versions of our app for iOS and Android. We have also written our test first, and then implemented the code to make the test pass following the TDD approach.

We hope this guide helps you to setup Detox in your project. We now have a great tool configured to continue writing end-to-end tests and make sure that any feature we add to our application is working as expected.