Emanuel Suriano Hi 👋 I build stuff with JavaScript 💻 Once a month I write an article ✍️ and sometimes I give talks 💬

React Native end-to-end testing with Detox

9 min read 2717

React Native Detox End To End Testing

Editor’s note: This article was last updated 28 January 2022 to address errors and updates to React Native tools.

End-to-end testing is a technique that is widely performed in the web ecosystem using frameworks and libraries like Cypress and Puppeteer. But when it comes to mobile applications, end-to-end testing is not as common. I have a theory that most mobile developers think testing mobile applications is hard, requiring a lot of setup and configuration, therefore they skip it.

In this article, we’ll cover how to implement the end-to-end testing framework Detox in a React Native application, write several interaction tests, and, finally, integrate Detox into your development workflow. For the sake of brevity, we’ll only focus on the iOS flow. Let’s get started!

You can access the full code for this project at the GitHub repository.

Table of contents

What is end-to-end testing?

In contrast to unit testing, end-to-end testing aims to cover as much of your application’s functionality as it can, simulating actual user actions. The more areas your test covers, the more reliable it will be. Therefore, an end-to-end test includes all the stages of an application: setting up your environment, installing your application, initialization, executing routines, and expecting events or behaviors to happen.

React Native end-to-end testing with Cypress

End-to-end testing using Cypress looks like the following image in the browser:

Cypress E2e Testing Browser

By selecting elements div, button, and input, using native selectors getElementById, getElementByName, and getElementByClassName, and then triggering events click, change, and focus, Cypress is able to create an instance of Chrome, run a URL, and begin interacting with the webpage.

At any point in the tests, the developer can expect something to happen or assert something to have a specific value. If all the expectations are true, the result of the test suite will be successful.

End-to-end testing in mobile applications

The process of testing mobile applications is actually quite similar to web applications. Let’s go through the steps:

We made a custom demo for .
No really. Click here to check it out.

  1. Create an instance of an emulator on an Android or iOS device
  2. Install the application
  3. Initialize the application
  4. Executing routines
  5. Expecting events

In a mobile app, end-to-end testing with Detox looks like the video below:

 

What is Detox testing framework?

Detox is an end-to-end framework for mobile apps developed by Wix, one of the top contributors to the React Native community. Wix also maintains amazing projects like react-native-navigation and react-native-ui-lib.

Detox provides great abstractions to select and trigger actions on elements. A normal test looks like the following code:

describe('Login flow', () => {
  it('should login successfully', async () => {
    await device.launchApp();
    // getting the reference of an element by ID and expecting to be visible
    await expect(element(by.id('email'))).toBeVisible();

    // Getting the reference and typing
    await element(by.id('email')).typeText('[email protected]');
    await element(by.id('password')).typeText('123456');

    // Getting the reference and executing a tap/press
    await element(by.text('Login')).tap();

    await expect(element(by.text('Welcome'))).toBeVisible();
    await expect(element(by.id('email'))).toNotExist();
  });
});

As you can see, the syntax is quite readable, and by using async/await, you can write synchronous, easily-understandable tests. Let’s jump into the demo!

Setting up our project

To focus on testing over setting up React Native, I suggest bootstrapping your project using react-native init, which creates a pretty simple and clean React Native project.

We’ll start by installing the dependency and creating a new project:

~ npm install react-native -g
~ react-native init testReactNativeDetox

               ######                ######
             ###     ####        ####     ###
            ##          ###    ###          ##
            ##             ####             ##
            ##             ####             ##
            ##           ##    ##           ##
            ##         ###      ###         ##
             ##  ########################  ##
          ######    ###            ###    ######
      ###     ##    ##              ##    ##     ###
   ###         ## ###      ####      ### ##         ###
  ##           ####      ########      ####           ##
 ##             ###     ##########     ###             ##
  ##           ####      ########      ####           ##
   ###         ## ###      ####      ### ##         ###
      ###     ##    ##              ##    ##     ###
          ######    ###            ###    ######
             ##  ########################  ##
            ##         ###      ###         ##
            ##           ##    ##           ##
            ##             ####             ##
            ##             ####             ##
            ##          ###    ###          ##
             ###     ####        ####     ###
               ######                ######


                  Welcome to React Native!
                 Learn Once Write Anywhere

✔ Downloading template
✔ Copying template
✔ Processing template
✔ Installing dependencies
✔ Installing CocoaPods dependencies (this may take a few minutes)

  Run instructions for iOS:
    • cd testReactNativeDetox && react-native run-ios
    - or -
    • Open testReactNativeDetox/ios/testReactNativeDetox.xcworkspace in Xcode or run "xed -b ios"
    • Hit the Run button

  Run instructions for Android:
    • Have an Android emulator running (quickest way to get started), or a device connected.
    • cd testReactNativeDetox && react-native run-android

After this step, you can try running the application in the emulator by executing the code below:

~ cd testReactNativeDetox
~ react-native run-ios

You should see an output like the following:

Emulator Output

Running React Native tests with Detox

Before we can start testing our React Native application with Detox, you’ll need to have the following:

  • Xcode installed
  • Homebrew installed and updated
  • Node.js installed. Use the command brew update && brew install node
  • applesimutils installed. Use brew tap wix/brew;brew install applesimutils;
  • detox-cli installed. Use npm install -g detox-cli

Start by adding Detox as a dependency for the project:

~ yarn add detox -D

Inside the CLI, you’ll find a command that can automatically set up the project. You’ll need to run the following:

~  detox init -r jest

detox[34202] INFO:  [init.js] Created a file at path: e2e/config.json
detox[34202] INFO:  [init.js] Created a file at path: e2e/init.js
detox[34202] INFO:  [init.js] Created a file at path: e2e/firstTest.spec.js
detox[34202] INFO:  [init.js] Patching package.json at path: /Users/USERNAME/Git/testReactNativeDetox/package.json
detox\[34202] INFO:  [init.js]   json["detox"\]["test-runner"] = "jest";

The code above will create a new folder called e2e with a basic test and some initial configuration like init.js, which is the file that tells Jest to start the simulator. Let’s modify the initial test to check if the two first sections are visible:

describe('Example', () => {
  beforeEach(async () => {
    await device.launchApp();
  });

  it('should have "Step One" section', async () => {
    await expect(element(by.text('Step One'))).toBeVisible();
  });

  it('should have "See Your Changes" section', async () => {
    await expect(element(by.text('See Your Changes'))).toBeVisible();
  });
});

Next, you need to add configuration for Detox inside your package.json or the .detoxrc.json file. If you’re using package.json, you need to add a detox section and add all the configurations shown below. Add the following object to the detox key, replacing the name of testReactNativeDetox with the name of your application at every occurrence:

{
  "detox": {
    "test-runner": "jest",
    "configurations": {
      "ios.release": {
        "binaryPath": "./ios/build/Build/Products/Release-iphonesimulator/testReactNativeDetox.app",
        "build": "xcodebuild -workspace ios/testReactNativeDetox.xcworkspace -configuration release -scheme testReactNativeDetox -sdk iphonesimulator -derivedDataPath ios/build",
        "type": "ios.simulator",
        "name": "iPhone X"
      }
    }
  }
}

To build the application, run the following command:

~ detox build

If your build failed with the message clang:error: linker command failed with exit code 1 (use-v to see invocation), please refer to this solution in GitHub issues and try running the command again. Remember, you need to perform the build process above every time you make changes to your application and want to test it.

If the command above still doesn’t work, check if you replaced all occurrences of your project name in package.json. If doing so fails as well, try opening up Xcode and building the debug version yourself.

Finally, we’ll run the test:

~ detox test

 PASS  e2e/firstTest.spec.js (7.514s)
  Example
    ✓ should have "Step One" section (260ms)
    ✓ should have "See Your Changes" section (278ms)

You should receive an output like the following:

Run Detox Test

To improve on our UI, let’s add a colorful image carousel. To save time, I decided to use an existing carousel component built by the community. For this demo, I used react-swipeable-views-native.
To generate random colors, I used randomColor.

Install both libraries as dependencies for the project:

~ yarn add react-swipeable-views-native randomcolor

Then, we’ll make a few modifications inside of our App.js file:

import React, {Fragment} from 'react';
import {
  SafeAreaView,
  StyleSheet,
  ScrollView,
  View,
  Text,
  TextInput,
  Button,
  StatusBar,
} from 'react-native';
import {
  Header,
  LearnMoreLinks,
  Colors,
  DebugInstructions,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import randomColor from 'randomcolor';
import SwipeableViews from 'react-swipeable-views-native';
const Slide = ({children}) => (
  <View
    style={[
      styles.slide,
      {backgroundColor: randomColor({luminosity: 'light'})},
    ]}>
    {children}
  </View>
);
const App = () => {
  return (
    <Fragment>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={styles.scrollView}>
          <Header />
          {global.HermesInternal == null ? null : (
            <View style={styles.engine}>
              <Text style={styles.footer}>Engine: Hermes</Text>
            </View>
          )}
          <View style={styles.body}>
            <SwipeableViews testID="slides">
              <Slide>
                <Text style={styles.sectionTitle}>Step One</Text>
                <Text style={styles.sectionDescription}>
                  Edit <Text style={styles.highlight}>App.js</Text> to change
                  this screen and then come back to see your edits.
                </Text>
              </Slide>
              <Slide>
                <Text style={styles.sectionTitle}>See Your Changes</Text>
                <Text style={styles.sectionDescription}>
                  <ReloadInstructions />
                </Text>
              </Slide>
              <Slide>
                <Text style={styles.sectionTitle}>Debug</Text>
                <Text style={styles.sectionDescription}>
                  <DebugInstructions />
                </Text>
                <Button onPress={() => alert('Clicked!')} title="Click here!" />
              </Slide>
              <Slide>
                <Text style={styles.sectionTitle}>Learn More</Text>
                <TextInput
                  testID="docsInput"
                  multiline
                  style={styles.sectionDescription}>
                  Read the docs to discover what to do next:
                </TextInput>
              </Slide>
            </SwipeableViews>
            <LearnMoreLinks />
          </View>
        </ScrollView>
      </SafeAreaView>
    </Fragment>
  );
};
const styles = StyleSheet.create({
  scrollView: {
    backgroundColor: Colors.lighter,
  },
  engine: {
    position: 'absolute',
    right: 0,
  },
  body: {
    backgroundColor: Colors.white,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
    color: Colors.black,
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
    color: Colors.dark,
  },
  highlight: {
    fontWeight: '700',
  },
  footer: {
    color: Colors.dark,
    fontSize: 12,
    fontWeight: '600',
    padding: 4,
    paddingRight: 12,
    textAlign: 'right',
  },
  slide: {
    padding: 24,
    height: 200,
    display: 'flex',
    justifyContent: 'center',
  },
});
export default App;

To enable swiping behavior, we wrapped all the sections inside SwipeableViews. We then wrapped each section inside a custom View called Slide, which implements properties like padding and backgroundColor. Lastly, we added a Button and a TextInput component to the last two slides.

The result is as follows:

Enable Swiping

Writing Detox tests

To make things easier, let’s add two new scripts into the package.json:

{
  "scripts": {
    "e2e:test": "detox test -c ios.release",
    "e2e:build": "detox build -c ios.release"
  }
}

Now that the application has changed, you need to create a new build of it in order to run tests with the modified version. Execute the following command:

~ yarn e2e:build

This process may take some time. In the meantime, let’s take a quick look at the existing tests:

describe('Example', () => {
  beforeEach(async () => {
    await device.launchApp();
  });

  it('should show "Step One"', async () => {
    await expect(element(by.text('Step One'))).toBeVisible();
  });

  it('should show "See Your Changes"', async () => {
    await expect(element(by.text('See Your Changes'))).toBeVisible(); // THIS TEST WILL FAIL!
  });
});

The See Your Changes section is now in the second slide of the carousel, which is not visible for the user until they swipe. Therefore, the second test will definitely fail. Let’s move Detox to that slide:

describe('Example', () => {
  // previous tests here

  it('should render "See Your Changes" in the second slide', async () => {
    // getting the reference of the slides and make a swipe
    await element(by.id('slides')).swipe('left');
    await expect(element(by.text('See Your Changes'))).toBeVisible(); // no this will pass!
    await element(by.id('slides')).swipe('right');
  });
});

We can use by.id() because we use the testID prop in the App component. At this point, you can execute the end-to-end tests with the following command, and they should pass:

~ yarn e2e:test

 PASS  e2e/firstTest.spec.js (7.514s)
  Example
    ✓ should have "Step One" section (260ms)
    ✓ should render "See Your Changes" in the second slide (993ms)

Second Test Fail Detox

Let’s add a few more tests to cover the following scenarios:

  • Test that the carousel allows the user to move back and forth inside the slides
  • Move the third slide and interact with the Button
  • Move the last slide and interact with the TextInput
describe('Example', () => {
  // previous tests here

  it('should enable swiping back and forth', async () => {
    await expect(element(by.text('Step One'))).toBeVisible();
    await element(by.id('slides')).swipe('left');
    await element(by.id('slides')).swipe('right');
    await expect(element(by.text('Step One'))).toBeVisible();
  });

  it('should render "Debug" and have a Button to click in the third slide', async () => {
    await element(by.id('slides')).swipe('left');
    await element(by.id('slides')).swipe('left');
    await expect(element(by.text('Debug'))).toBeVisible();

    await element(by.text('Click here!')).tap();
    await expect(element(by.text('Clicked!'))).toBeVisible();
    await element(by.text('OK')).tap();
  });

  it('should render "Learn More" and change text in the fourth slide', async () => {
    await element(by.id('slides')).swipe('left');
    await element(by.id('slides')).swipe('left');
    await element(by.id('slides')).swipe('left');
    await expect(element(by.text('Learn More'))).toBeVisible();

    const docsInput = element(by.id('docsInput'));

    await expect(docsInput).toBeVisible();

    await docsInput.clearText();
    await docsInput.typeText('Maybe later!');

    await expect(docsInput).toHaveText('Maybe later!');
  });
});

Let’s run the tests again:

~ yarn e2e:test

 PASS  e2e/firstTest.spec.js (22.128s)
  Example
    ✓ should have "Step One" section (268ms)
    ✓ should render "See Your Changes" in the second slide (982ms)
    ✓ should enable swiping back and forth (1861ms)
    ✓ should render "Debug" and have a Button to click in the third slide (2710ms)
    ✓ should render "Learn More" and change text in the fourth slide (9964ms)

Your app should now look like the following:

Full e2e Test Detox

Running E2E test in CI

Running tests inside CI is important, basically eliminating the need for manual testing and preventing shipping bugs to production. For this example, I decided to use TravisCI because it has an amazing integration with GitHub and also provides an unlimited plan for open source projects.

If you’re using GitHub, you can install the Travis application, create a new plan, and allow it to access your repositories. After that, you need to create a new file inside your project called .travis.yml, which defines the steps you want to run in CI.

I tweaked the CI configuration inside the official Detox docs slightly. Below is the configuration that worked in my case:

language: objective-c
osx_image: xcode10.2

branches:
  only:
    - master

env:
  global:
    - NODE_VERSION=stable

install:
  - brew tap wix/brew
  - brew install applesimutils
  - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash
  - export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
  - nvm install $NODE_VERSION
  - nvm use $NODE_VERSION
  - nvm alias default $NODE_VERSION

  - npm install -g react-native-cli
  - npm install -g detox-cli
  - npm install
  - cd ios; pod install; cd -;

script:
  - npm run e2e:ci

Finally, add the command e2e:ci into your package.json. The detox build command will build the application, detox test will run the tests, and the --cleanup flag will close the emulator to finish the execution:

{
  "scripts": {
    "e2e:test": "detox test -c ios.release",
    "e2e:build": "detox build -c ios.release",
    "e2e:ci": "npm run e2e:build && npm run e2e:test -- --cleanup"
  }
}

Once you’ve pushed all the changes into your master branch, try to open a new pull request. You should see a new pull request checker has been added, which we’ll call Travis, which runs the Detox tests:

Travis Run Detox Tests

You can access the full log in Travis for that pull request.

Conclusion

I highly encourage you try Detox for testing your React Native applications. Detox is an amazing end-to-end testing solution for mobile. After using it for quite some time, I’d say it has the following pros and cons:

  • Very well abstracted syntax for matchers and to trigger specific actions
  • Great integration with Jest
  • Ability to run tests in CI

Sometimes, you might encounter configuration errors, and finding the proper solution may take some time. The best way to address this problem is to go and take a deep look at GitHub Issues. I hope you enjoyed this article, happy coding!

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — .

Emanuel Suriano Hi 👋 I build stuff with JavaScript 💻 Once a month I write an article ✍️ and sometimes I give talks 💬

2 Replies to “React Native end-to-end testing with Detox”

  1. Pretty good intro to E2E with Detox. One thing I noticed was that I needed to run “applesimutils –list” on the console to get a valid name for my device to replace “iPhone X” with “iPhone 11” since that is the emulator I’m currently running. Might be a good idea to add that as a note to this article.

Leave a Reply