--- title: Running E2E tests on EAS Build sidebar_title: Running E2E tests --- import { Collapsible } from '~/ui/components/Collapsible'; import { Terminal, DiffBlock } from '~/ui/components/Snippet'; import ImageSpotlight from '~/components/plugins/ImageSpotlight'; > **Warning** EAS Build support for E2E testing is in a _very early_ state. The intention of this guide is to explain how you can run E2E tests on the service today, > without all of the affordances that we plan to build in the future. This guide will evolve over time as support for testing workflows in EAS Build improves. With EAS Build, you can build a workflow for running E2E tests for your application. In this guide, you will learn how to use one of the most popular libraries ([Detox](https://wix.github.io/Detox)) to do that. This guide explains how to run E2E tests with Detox in a bare workflow project. You can use [`@config-plugins/detox`](https://github.com/expo/config-plugins/tree/main/packages/detox) for a managed project, but you may need to adjust some of the instructions in this guide in order to do so. ## Running iOS tests ### 1. Initialize a new Bare Workflow project Let's start by initializing a new Expo project, installing and configuring `@config-plugins/detox`, and running `npx expo prebuild` to generate the native projects. Start with the following commands: Now, open **app.json** and add the `@config-plugins/detox` plugin to your `plugins` list (this must be done before prebuilding). This will automatically configure the Android native code to support Detox. ```json app.json { "expo": { // ... "plugins": ["@config-plugins/detox"] } } ``` Run prebuild to generate the native projects: ### 2. Make home screen interactive The first step to writing E2E tests is to have something to test - we have an empty app, so let's make our app interactive. We can add a button and display some new text when it's pressed. Later, we're going to write a test that's going to tap the button and check whether the text has been displayed.
```js App.js import { StatusBar } from 'expo-status-bar'; import { useState } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; export default function App() { const [clicked, setClicked] = useState(false); return ( {!clicked && ( setClicked(true)}> Click me )} {clicked && Hi!} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, hi: { fontSize: 30, color: '#4630EB', }, button: { alignItems: 'center', justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 32, borderRadius: 4, elevation: 3, backgroundColor: '#4630EB', }, text: { fontSize: 16, lineHeight: 21, fontWeight: 'bold', letterSpacing: 0.25, color: 'white', }, }); ``` ### 3. Set up Detox #### Install dependencies Let's add two development dependencies to the project - `jest` and `detox`. `jest` (or `mocha`) is required because `detox` does not have its own test-runner. > See the official Detox docs at https://wix.github.io/Detox/docs/introduction/getting-started/ and https://wix.github.io/Detox/docs/guide/jest to learn about any potential updates to this process. #### Configure Detox Detox requires you to specify both the build command and path to the binary produced by it. Technically, the build command is not necessary when running tests on EAS Build, but allows you to run tests locally (for example, using `npx detox build --configuration ios.release`). Edit **.detoxrc.json** and replace the configuration with: ```json .detoxrc.json { "testRunner": "jest", "runnerConfig": "e2e/config.json", "skipLegacyWorkersInjection": true, "apps": { "android.release": { "type": "android.apk", "build": "cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..", "binaryPath": "android/app/build/outputs/apk/release/app-release.apk" }, "ios.release": { "type": "ios.app", /* @info This is project-specific, replace eastestsexample with correct value */ "build": "xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build", "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app" /* @end */ } }, "devices": { "emulator": { "type": "android.emulator", "device": { "avdName": "pixel_4" } }, "simulator": { "type": "ios.simulator", "device": { "type": "iPhone 11" } } }, "configurations": { "android.release": { "device": "emulator", "app": "android.release" }, "ios.release": { "device": "simulator", "app": "ios.release" } } } ``` ### 4. Write E2E tests Next, we'll add our first E2E tests. Delete the auto-generated **e2e/firstTest.e2e.js** and create our own **e2e/homeScreen.e2e.js** with the following contents: ```js e2e/homeScreen.e2e.js describe('Home screen', () => { beforeAll(async () => { await device.launchApp(); }); beforeEach(async () => { await device.reloadReactNative(); }); it('"Click me" button should be visible', async () => { await expect(element(by.id('click-me-button'))).toBeVisible(); }); it('shows "Hi!" after tapping "Click me"', async () => { await element(by.id('click-me-button')).tap(); await expect(element(by.text('Hi!'))).toBeVisible(); }); }); ``` There are two tests in the suite: - One that checks whether the "Click me" button is visible on the home screen. - Another that verifies that tapping the button triggers displaying "Hi!". Both tests assume the button has the `testID` set to `click-me-button`. See [the source code](#2-make-home-screen-interactive) for details. ### 5. Configure EAS Build Now that we have configured Detox and written our first E2E test, let's configure EAS Build and run the tests in the cloud. #### Create eas.json The following command creates [eas.json](/build/eas-json.mdx) in the project's root directory: #### Configure EAS Build There are a few more steps to configure EAS Build for running E2E tests as part of the build: - Android tests: - Tests are run in the Android Emulator. You will define a build profile that builds your app for the emulator (produces an `apk` file). - Install the emulator and all its system dependencies. - iOS test: - Tests are run in the iOS simulator. You will define a build profile that builds your app for the simulator. - Install the [`applesimutils`](https://github.com/wix/AppleSimulatorUtils) command line util. - Configure EAS Build to run Detox tests after successfully building the app. Edit **eas.json** and add the `test` build profile: ```json eas.json { "build": { "test": { "android": { "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release", "withoutCredentials": true }, "ios": { "simulator": true } } } } ``` Create **eas-hooks/eas-build-pre-install.sh** that installs the necessary tools and dependencies for the given platform: ```sh eas-hooks/eas-build-pre-install.sh #!/usr/bin/env bash set -eox pipefail if [[ "$EAS_BUILD_RUNNER" == "eas-build" && "$EAS_BUILD_PROFILE" == "test"* ]]; then if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then sudo apt-get --quiet update --yes # Install emulator & video bridge dependencies # Source: https://github.com/react-native-community/docker-android/blob/master/Dockerfile sudo apt-get --quiet install --yes \ libc6 \ libdbus-1-3 \ libfontconfig1 \ libgcc1 \ libpulse0 \ libtinfo5 \ libx11-6 \ libxcb1 \ libxdamage1 \ libnss3 \ libxcomposite1 \ libxcursor1 \ libxi6 \ libxext6 \ libxfixes3 \ zlib1g \ libgl1 \ pulseaudio \ socat sdkmanager --install "system-images;android-32;google_apis;x86_64" avdmanager --verbose create avd --force --name "pixel_4" --device "pixel_4" --package "system-images;android-32;google_apis;x86_64" else brew tap wix/brew brew install applesimutils fi fi ``` Next, create **eas-hooks/eas-build-on-success.sh** with the following contents. The script runs different commands for Android and iOS. For iOS, the only command is `detox test`. For Android, it's a bit more complicated. You'll have to start the emulator prior to running the tests as `detox` sometimes seems to be having problems with starting the emulator on its own and it can get stuck on running the first test from your test suite. After the `detox test` run, there is a command that kills the previously started emulator. ```sh eas-hooks/eas-build-on-success.sh #!/usr/bin/env bash set -eox pipefail ANDROID_EMULATOR=pixel_4 if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then # Start emulator $ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null & # Wait for emulator max_retry=10 counter=0 until adb shell getprop sys.boot_completed; do sleep 10 [[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1 counter=$((counter + 1)) done # Run tests detox test --configuration android.release --headless # Kill emulator adb emu kill else detox test --configuration ios.release --headless fi fi ``` Edit **package.json** to use [EAS Build hooks](/build-reference/npm-hooks.mdx) to run the above scripts on EAS Build: ```json package.json { "scripts": { "eas-build-pre-install": "./eas-hooks/eas-build-pre-install.sh", "eas-build-on-success": "./eas-hooks/eas-build-on-success.sh" } } ``` > Don't forget to add executable permissions to **eas-build-pre-install.sh** and **eas-build-on-success.sh**. Run `chmod +x eas-hooks/*.sh`. ### 5.1. Patch **build.gradle** > This step will be redundant in the future. The Android build command that you use to produce the test build is `./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release`. Notice that it consists of two Gradle tasks. Unfortunately, when building the `*AndroidTest` task, some versions of the `expo-modules-core` module change what native libraries are included in the app binary. Those settings don't work with settings for `assembleRelease`. To fix the problem, add the `pickFirsts` list under `android.packagingOptions` in your **android/app/build.gradle**. The `pickFirsts` property overrides the setting for your project. ### 6. Run tests on EAS Build Running the tests on EAS Build is like running a regular build: If you have set up everything correctly you should see the successful test result in the build logs: ### 7. Upload screenshots of failed test cases > This step is optional but highly recommended. When an E2E test case fails, it can be helpful to see the screenshot of the application state. EAS Build makes it easy to upload any arbitrary build artifacts using the [`buildArtifactPaths`](/build-reference/eas-json.mdx#buildartifactpaths) field in **eas.json**. #### Take screenshots for failed tests [Detox supports taking in-test screenshots of the device](https://wix.github.io/Detox/docs/api/screenshots). It exposes the `device.takeScreenshot()` function that can be called from any test case. Neither `jest` nor `detox` offers a simple way to detect when a particular test case has failed and take a screenshot for only failed test cases. You will have to implement your own mechanism for that. Edit **e2e/environment.js** and add the `handleTestEvent` method to the `CustomDetoxEnvironment` class. The function handles test events and sets a global `testFailed` variable for test case failure. See the snippet below: ```ts e2e/environment.js class CustomDetoxEnvironment extends DetoxCircusEnvironment { // ... async handleTestEvent(event, state) { const { name } = event; if (['test_start', 'test_fn_start'].includes(name)) { this.global.testFailed = false; } if (name === 'test_fn_failure') { this.global.testFailed = true; } await super.handleTestEvent(event, state); } } ``` After modifying the environment class, define an `afterEach` hook that calls `device.takeScreenshot()` for failed tests. Create the **e2e/setup.js** file with the following snippet: ```ts e2e/setup.js afterEach(async () => { if (testFailed) { await device.takeScreenshot('screenshot'); } }); ``` The last step is to configure `jest` to load `e2e/setup.js` before running tests. This way, you don't need to include the `afterEach` hook in every test suite: ```json e2e/config.json { /// ... "setupFilesAfterEnv": ["./setup.js"] } ``` After making those changes, screenshots for failed tests will be saved in the `artifacts` directory. #### Configure EAS Build for screenshots upload Edit **eas.json** and add `buildArtifactPaths` to the `test` build profile: ```json eas.json { "build": { "test": { "android": { "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release", "withoutCredentials": true }, "ios": { "simulator": true }, /* @info */ "buildArtifactPaths": ["artifacts/**/*.png"] /* @end */ } } } ``` In contrast to `applicationArchivePath`, the build artifacts defined at `buildArtifactPaths` will be uploaded even if the build fails. All `.png` files from the `artifacts` directory will be packed into a tarball and uploaded to AWS S3. You can download them later from the build details page. If you run E2E tests locally, remember to add `artifacts` to `.gitignore`: ```stylus .gitignore artifacts/ ``` #### Break a test and run a build To test the new configuration, let's break a test and see that EAS Build uploads the screenshots. Edit **e2e/homeScreen.e2e.js** and make the following change: Run an iOS build with the following command and wait for it to finish: After going to the build details page you should see that the build failed. Use the **"Download artifacts"** button to download and examine the screenshot: ## Repository The full example from this guide is available at https://github.com/expo/eas-tests-example. ## Alternative Approaches ### Using development builds to speed up test run time > **Warning** This might not work properly on Android. The above guide explains how to run E2E tests against a release build of your project, which requires executing a full native build before each test run. Re-building the native app each time you run E2E tests may not be desirable if only the project JavaScript or assets have changed. However, this is necessary for release builds because the app JavaScript bundle is embedded into the binary. Instead, we can use [development builds](/development/introduction/) to load from a local development server or from [published updates](/eas-update/introduction/) to save time and CI resources. This can be done by having your E2E test runner invoke the app with a URL that points to a specific update bundle URL, as described in the [development builds deep linking URLs guide](/development/development-workflows/#deep-linking-urls). Development builds typically display an onboarding welcome screen when an app is launched for the first time, which intends to provide context about the `expo-dev-client` UI for developers. However, it can interfere with your E2E tests (which expect to interact with your app and not an onboarding screen). To skip the onboarding screen in a test environment, the query parameter `disableOnboarding=1` can be appended to the project URL (an EAS Update URL or a local development server URL). An example of such a Detox test is shown below. Full example code is available on the [eas-tests-example](https://github.com/expo/eas-tests-example) repository. ```js /* @info New line */ const { openAppForDebugBuild } = require('./utils/openAppForDebugBuild'); /* @end */ describe('Home screen', () => { beforeEach(async () => { await device.launchApp({ newInstance: true, }); /* @info New line */ await openAppForDebugBuild(); /* @end */ }); it('"Click me" button should be visible', async () => { await expect(element(by.id('click-me-button'))).toBeVisible(); }); it('shows "Hi!" after tapping "Click me"', async () => { await element(by.id('click-me-button')).tap(); await expect(element(by.text('Hi!'))).toBeVisible(); }); }); ``` ```js const appConfig = require('../../../app.json'); module.exports.openAppForDebugBuild = async function openAppForDebugBuild() { const [platform, target] = process.env.DETOX_CONFIGURATION.split('.'); if (target !== 'debug') { return; } await sleep(1000); await device.openURL({ url: process.env.EXPO_USE_UPDATES ? // Testing latest published EAS update for the test_debug channel getDeepLinkUrl(getLatestUpdateUrl()) : // Local testing with packager getDeepLinkUrl(getDevLauncherPackagerUrl(platform)), }); await sleep(3000); }; const getDeepLinkUrl = url => /* @info This is project-specific, replace eastestsexample with correct value */ `eastestsexample://expo-development-client/?url=${encodeURIComponent(url)}`; /* @end */ const getDevLauncherPackagerUrl = platform => `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`; const getLatestUpdateUrl = () => `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`; const getAppId = () => appConfig?.expo?.extra?.eas?.projectId ?? ''; const sleep = t => new Promise(res => setTimeout(res, t)); ```