Run Detox Tests with GitHub Actions, Part 1: Add a New Workflow

Automated end-to-end testing is an important part of mobile app development, but it’s not always straightforward to set up. My team uses Detox to test various workflows in our React Native app, and we recently got the test cases running on both iOS and Android using GitHub Actions.

In this series, I’ll walk through the steps we took to move from a set of locally passing Detox tests to a fully automated and parallelized process.

We’ll start by creating the simplest version of the workflow that runs all the tests in sequence.

0. Create a new workflow.

Before you start building or testing any of your application code, we need add a new workflow to the repo with some placeholder jobs:


name: Run Detox Tests
on: [pull_request]

jobs:

    build-and-test-ios:
        name: iOS - Build and run Detox tests
        runs-on: macos-13

        steps:
            - name: Checkout Code
              uses: actions/checkout@v4
              with:
                fetch-depth: 1

    build-and-test-android:
        name: Android - Build and run Detox tests
        runs-on: ubuntu-latest

        steps:
            - name: Checkout Code
              uses: actions/checkout@v4
              with:
                fetch-depth: 1

This workflow will run the iOS and Android tests in parallel on all pull requests. You can adjust the trigger to match your process. For now, we’re just checking out the repo.

Note that we’re using macOS 13 instead of 14 (ie macos-latest). You may be able to use 14, but my team had some trouble with it so we’re sticking with 13 for the time being.

1. Build the app.

When we set up Detox initially, we added a couple of scripts to our package.json to build the apps:


{
    "scripts": {
        "detox:android:build": "detox build --configuration android.emu.debug",
        "detox:ios:build": "detox build --configuration ios.sim.debug"
    }
}

We’re going to use these same scripts in the workflow, but first we need to make sure that all of our dependencies are available.

For the iOS job, we need to setup Node and then do an npm install and a pod install:


    build-and-test-ios:
        name: iOS - Build and run Detox tests
        runs-on: macos-latest

        steps:
            - name: Checkout Code
              uses: actions/checkout@v4
              with:
                fetch-depth: 1

            - name: Setup Node.js
              uses: actions/setup-node@v4
              with:
                node-version: 22.2.0

            - name: Npm install
              run: npm install

            - name: Pod install
              run: cd ios && pod install

            - name: Build Detox
              run: npm run detox:ios:build

For Android, we need to set up Node and Java, and do an npm install.



    build-and-test-android:
        name: Android - Build and run Detox tests
        runs-on: ubuntu-latest

        steps:
            - name: Checkout Code
              uses: actions/checkout@v4
              with:
                fetch-depth: 1

            - name: Setup Node.js
              uses: actions/setup-node@v4
              with:
                node-version: 22.2.0

            - name: Setup Java
              uses: actions/setup-java@v4
              with:
                distribution: zulu
                java-version: 17
            
            - name: Npm install
              run: npm install

            - name: Build Detox
              run: npm run detox:android:build

You can adjust the Node and Java versions to match your project requirements.

2. Start the Metro server.

You’ll need to the Metro server running in order to execute the tests.

We added another script to our package.json so that we could customize some of the application’s behavior in test mode (like mocking out network requests). If you don’t need to do this, just run the normal command to start your Metro server.


{
    "scripts": {
        "detox:start": "E2E_TESTS='true' react-native start"
    }
}

Add the following step to both the iOS and Android jobs:


- name: Start Metro Server
  run: npm run detox:start &

3. Run the tests.

Once again, we have custom scripts in the package.json to run the tests in headless mode and take videos/screenshots in case the tests fail:


{
    "scripts": {
        "detox:android:test:ci": "detox test --configuration android.emu.debug --headless --record-logs failing --record-videos failing --take-screenshots failing",
        "detox:ios:test:ci": "detox test --configuration ios.sim.debug --headless --record-logs failing --record-videos failing --take-screenshots failing"
    }
}

On iOS, we need to install applesimutils, and then we’re all set to run the tests:


- name: Install Applesimutils
  run: |
      brew tap wix/brew
      brew install applesimutils

- name: Run Detox tests
  run: npm run detox:ios:test:ci

This should launch the simulator automatically and execute the tests.

Android is a little more complicated. We have to launch an emulator before we can start running the tests. We found the Android Emulator Runner action to be a great solution for this.


- name: Run Detox tests
  uses: reactivecircus/android-emulator-runner@v2
  with:
      api-level: 31
      arch: x86_64
      avd-name: Android_API31
      force-avd-creation: false
      emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
      disable-animations: false
      script: npm run detox:android:test:ci

Adjust the API level to match whatever your app is targeting. You also need to make sure that the avd-name matches the devices.emulator.device.avdName from your .detoxrc.js file.

4. Upload test artifacts.

Chances are, at least one of your tests will fail when you run it on CI for the first time. One common problem that we encountered was the keyboard covering up text inputs since we always ran tests locally with the hardware keyboard instead of the virtual device’s keyboard.

To figure out why your tests failed, you probably will need to examine screenshots and the video recording of the test run. You can upload the artifacts for any failed tests as the final step in each workflow:


# iOS
- name: Upload Test Artifact
  if: failure()
  uses: actions/upload-artifact@v4
    with:
        name: detox-ios-artifacts
        path: artifacts
        retention-days: 5

# Android
- name: Upload Test Artifact
  if: failure()
  uses: actions/upload-artifact@v4
    with:
        name: detox-android-artifacts
        path: artifacts
        retention-days: 5

Finally, fix “out of space” errors.

Test case failures might not be the only reason for a CI failure. One common problem that we encountered was running out of space on the GitHub runners.

If you encounter this on your iOS job, you can simply use one of the larger macOS runners. They do cost more, but they are also a lot faster. My team uses macos-13-xlarge.

Fixing the problem on Android requires a more creative solution. Larger Ubuntu runners are configured at the organization or enterprise level in GitHub, so you can’t just swap out the runs-on setting. Instead, you can use the Free Disk Space action, which will delete unnecessary tools for the runner. Just add the following step at the very beginning of your Android job:


- name: Free Disk Space (Ubuntu)
  uses: jlumbroso/free-disk-space@main
  with:
    tool-cache: true
    android: false

Next steps

At this point, you should have a workflow that builds the app for both platforms and then runs each test sequentially. You can see the full workflow YAML file here.

Depending on how many test cases you have, this could take a very long time — an hour or more. Unless you have endless patience and no deadlines, this is probably not an acceptable amount of time.

In the next post, we’ll add some caching to speed up the build and handle parallelizing the test execution.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *