1--- 2title: Running E2E tests on EAS Build 3sidebar_title: Running E2E tests 4--- 5 6import { Collapsible } from '~/ui/components/Collapsible'; 7import { Terminal, DiffBlock } from '~/ui/components/Snippet'; 8import ImageSpotlight from '~/components/plugins/ImageSpotlight'; 9 10> **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, 11> 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. 12 13With 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. 14 15This 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. 16 17## Running iOS tests 18 19### 1. Initialize a new Bare Workflow project 20 21Let's start by initializing a new Expo project, installing and configuring `@config-plugins/detox`, and running `npx expo prebuild` to generate the native projects. 22 23Start with the following commands: 24 25<Terminal 26 cmd={[ 27 '# Initialize a new project', 28 '$ npx create-expo-app eas-tests-example', 29 '# cd into the project directory', 30 '$ cd eas-tests-example', 31 '# Install @config-plugins/detox', 32 '$ npm install --save-dev @config-plugins/detox', 33 ]} 34/> 35 36Now, 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. 37 38```json app.json 39{ 40 "expo": { 41 // ... 42 "plugins": ["@config-plugins/detox"] 43 } 44} 45``` 46 47Run prebuild to generate the native projects: 48 49<Terminal cmd={['$ npx expo prebuild']} /> 50 51### 2. Make home screen interactive 52 53The 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. 54Later, we're going to write a test that's going to tap the button and check whether the text has been displayed. 55 56<div style={{ display: 'flex', justifyContent: 'center' }}> 57 <img src="/static/images/eas-build/tests/01-click-me.png" style={{ maxWidth: '45%' }} /> 58 <img src="/static/images/eas-build/tests/02-hi.png" style={{ maxWidth: '45%' }} /> 59</div> 60 61<Collapsible summary=" See the source code"> 62 63```js App.js 64import { StatusBar } from 'expo-status-bar'; 65import { useState } from 'react'; 66import { Pressable, StyleSheet, Text, View } from 'react-native'; 67 68export default function App() { 69 const [clicked, setClicked] = useState(false); 70 71 return ( 72 <View style={styles.container}> 73 {!clicked && ( 74 <Pressable testID="click-me-button" style={styles.button} onPress={() => setClicked(true)}> 75 <Text style={styles.text}>Click me</Text> 76 </Pressable> 77 )} 78 {clicked && <Text style={styles.hi}>Hi!</Text>} 79 <StatusBar style="auto" /> 80 </View> 81 ); 82} 83 84const styles = StyleSheet.create({ 85 container: { 86 flex: 1, 87 backgroundColor: '#fff', 88 alignItems: 'center', 89 justifyContent: 'center', 90 }, 91 hi: { 92 fontSize: 30, 93 color: '#4630EB', 94 }, 95 button: { 96 alignItems: 'center', 97 justifyContent: 'center', 98 paddingVertical: 12, 99 paddingHorizontal: 32, 100 borderRadius: 4, 101 elevation: 3, 102 backgroundColor: '#4630EB', 103 }, 104 text: { 105 fontSize: 16, 106 lineHeight: 21, 107 fontWeight: 'bold', 108 letterSpacing: 0.25, 109 color: 'white', 110 }, 111}); 112``` 113 114</Collapsible> 115 116### 3. Set up Detox 117 118#### Install dependencies 119 120Let'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. 121 122<Terminal 123 cmd={[ 124 '# Install jest & detox', 125 '$ npm install --save-dev jest detox', 126 '# Create Detox configuration files', 127 '$ npx detox init -r jest', 128 ]} 129/> 130 131> 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. 132 133#### Configure Detox 134 135Detox 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`). 136 137Edit **.detoxrc.json** and replace the configuration with: 138 139```json .detoxrc.json 140{ 141 "testRunner": "jest", 142 "runnerConfig": "e2e/config.json", 143 "skipLegacyWorkersInjection": true, 144 "apps": { 145 "android.release": { 146 "type": "android.apk", 147 "build": "cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..", 148 "binaryPath": "android/app/build/outputs/apk/release/app-release.apk" 149 }, 150 "ios.release": { 151 "type": "ios.app", 152 /* @info This is project-specific, replace eastestsexample with correct value */ 153 "build": "xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build", 154 "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app" 155 /* @end */ 156 } 157 }, 158 "devices": { 159 "emulator": { 160 "type": "android.emulator", 161 "device": { 162 "avdName": "pixel_4" 163 } 164 }, 165 "simulator": { 166 "type": "ios.simulator", 167 "device": { 168 "type": "iPhone 11" 169 } 170 } 171 }, 172 "configurations": { 173 "android.release": { 174 "device": "emulator", 175 "app": "android.release" 176 }, 177 "ios.release": { 178 "device": "simulator", 179 "app": "ios.release" 180 } 181 } 182} 183``` 184 185### 4. Write E2E tests 186 187Next, 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: 188 189```js e2e/homeScreen.e2e.js 190describe('Home screen', () => { 191 beforeAll(async () => { 192 await device.launchApp(); 193 }); 194 195 beforeEach(async () => { 196 await device.reloadReactNative(); 197 }); 198 199 it('"Click me" button should be visible', async () => { 200 await expect(element(by.id('click-me-button'))).toBeVisible(); 201 }); 202 203 it('shows "Hi!" after tapping "Click me"', async () => { 204 await element(by.id('click-me-button')).tap(); 205 await expect(element(by.text('Hi!'))).toBeVisible(); 206 }); 207}); 208``` 209 210There are two tests in the suite: 211 212- One that checks whether the "Click me" button is visible on the home screen. 213- Another that verifies that tapping the button triggers displaying "Hi!". 214 215Both tests assume the button has the `testID` set to `click-me-button`. See [the source code](#2-make-home-screen-interactive) for details. 216 217### 5. Configure EAS Build 218 219Now that we have configured Detox and written our first E2E test, let's configure EAS Build and run the tests in the cloud. 220 221#### Create eas.json 222 223The following command creates [eas.json](/build/eas-json.mdx) in the project's root directory: 224 225<Terminal cmd={['$ eas build:configure']} /> 226 227#### Configure EAS Build 228 229There are a few more steps to configure EAS Build for running E2E tests as part of the build: 230 231- Android tests: 232 - Tests are run in the Android Emulator. You will define a build profile that builds your app for the emulator (produces an `apk` file). 233 - Install the emulator and all its system dependencies. 234- iOS test: 235 - Tests are run in the iOS simulator. You will define a build profile that builds your app for the simulator. 236 - Install the [`applesimutils`](https://github.com/wix/AppleSimulatorUtils) command line util. 237- Configure EAS Build to run Detox tests after successfully building the app. 238 239Edit **eas.json** and add the `test` build profile: 240 241```json eas.json 242{ 243 "build": { 244 "test": { 245 "android": { 246 "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release", 247 "withoutCredentials": true 248 }, 249 "ios": { 250 "simulator": true 251 } 252 } 253 } 254} 255``` 256 257Create **eas-hooks/eas-build-pre-install.sh** that installs the necessary tools and dependencies for the given platform: 258 259```sh eas-hooks/eas-build-pre-install.sh 260#!/usr/bin/env bash 261 262set -eox pipefail 263 264if [[ "$EAS_BUILD_RUNNER" == "eas-build" && "$EAS_BUILD_PROFILE" == "test"* ]]; then 265 if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then 266 sudo apt-get --quiet update --yes 267 268 # Install emulator & video bridge dependencies 269 # Source: https://github.com/react-native-community/docker-android/blob/master/Dockerfile 270 sudo apt-get --quiet install --yes \ 271 libc6 \ 272 libdbus-1-3 \ 273 libfontconfig1 \ 274 libgcc1 \ 275 libpulse0 \ 276 libtinfo5 \ 277 libx11-6 \ 278 libxcb1 \ 279 libxdamage1 \ 280 libnss3 \ 281 libxcomposite1 \ 282 libxcursor1 \ 283 libxi6 \ 284 libxext6 \ 285 libxfixes3 \ 286 zlib1g \ 287 libgl1 \ 288 pulseaudio \ 289 socat 290 291 sdkmanager --install "system-images;android-32;google_apis;x86_64" 292 avdmanager --verbose create avd --force --name "pixel_4" --device "pixel_4" --package "system-images;android-32;google_apis;x86_64" 293 else 294 brew tap wix/brew 295 brew install applesimutils 296 fi 297fi 298 299``` 300 301Next, 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. 302 303```sh eas-hooks/eas-build-on-success.sh 304#!/usr/bin/env bash 305 306set -eox pipefail 307 308ANDROID_EMULATOR=pixel_4 309 310if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then 311 if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then 312 # Start emulator 313 $ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null & 314 315 # Wait for emulator 316 max_retry=10 317 counter=0 318 until adb shell getprop sys.boot_completed; do 319 sleep 10 320 [[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1 321 counter=$((counter + 1)) 322 done 323 324 # Run tests 325 detox test --configuration android.release --headless 326 327 # Kill emulator 328 adb emu kill 329 else 330 detox test --configuration ios.release --headless 331 fi 332fi 333``` 334 335Edit **package.json** to use [EAS Build hooks](/build-reference/npm-hooks.mdx) to run the above scripts on EAS Build: 336 337```json package.json 338{ 339 "scripts": { 340 "eas-build-pre-install": "./eas-hooks/eas-build-pre-install.sh", 341 "eas-build-on-success": "./eas-hooks/eas-build-on-success.sh" 342 } 343} 344``` 345 346> 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`. 347 348### 5.1. Patch **build.gradle** 349 350> This step will be redundant in the future. 351 352The 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`. 353 354To 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. 355 356<DiffBlock source="/static/diffs/e2e-tests-pickfirsts.diff" /> 357 358### 6. Run tests on EAS Build 359 360Running the tests on EAS Build is like running a regular build: 361 362<Terminal cmd={['$ eas build -p all -e test']} /> 363 364If you have set up everything correctly you should see the successful test result in the build logs: 365 366<ImageSpotlight src="/static/images/eas-build/tests/03-logs.png" style={{ maxWidth: '90%' }} /> 367 368### 7. Upload screenshots of failed test cases 369 370> This step is optional but highly recommended. 371 372When 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**. 373 374#### Take screenshots for failed tests 375 376[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. 377 378Neither `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. 379 380Edit **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: 381 382```ts e2e/environment.js 383class CustomDetoxEnvironment extends DetoxCircusEnvironment { 384 // ... 385 386 async handleTestEvent(event, state) { 387 const { name } = event; 388 389 if (['test_start', 'test_fn_start'].includes(name)) { 390 this.global.testFailed = false; 391 } 392 393 if (name === 'test_fn_failure') { 394 this.global.testFailed = true; 395 } 396 397 await super.handleTestEvent(event, state); 398 } 399} 400``` 401 402After 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: 403 404```ts e2e/setup.js 405afterEach(async () => { 406 if (testFailed) { 407 await device.takeScreenshot('screenshot'); 408 } 409}); 410``` 411 412The 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: 413 414```json e2e/config.json 415{ 416 /// ... 417 "setupFilesAfterEnv": ["./setup.js"] 418} 419``` 420 421After making those changes, screenshots for failed tests will be saved in the `artifacts` directory. 422 423#### Configure EAS Build for screenshots upload 424 425Edit **eas.json** and add `buildArtifactPaths` to the `test` build profile: 426 427```json eas.json 428{ 429 "build": { 430 "test": { 431 "android": { 432 "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release", 433 "withoutCredentials": true 434 }, 435 "ios": { 436 "simulator": true 437 }, 438 /* @info */ 439 "buildArtifactPaths": ["artifacts/**/*.png"] 440 /* @end */ 441 } 442 } 443} 444``` 445 446In 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. 447 448If you run E2E tests locally, remember to add `artifacts` to `.gitignore`: 449 450```stylus .gitignore 451artifacts/ 452``` 453 454#### Break a test and run a build 455 456To test the new configuration, let's break a test and see that EAS Build uploads the screenshots. 457 458Edit **e2e/homeScreen.e2e.js** and make the following change: 459 460<DiffBlock source="/static/diffs/e2e-tests-homescreen.diff" /> 461 462Run an iOS build with the following command and wait for it to finish: 463 464<Terminal cmd={['$ eas build -p ios -e test']} /> 465 466After going to the build details page you should see that the build failed. Use the **"Download artifacts"** button to download and examine the screenshot: 467 468<ImageSpotlight src="/static/images/eas-build/tests/04-artifacts.png" style={{ maxWidth: '90%' }} /> 469 470## Repository 471 472The full example from this guide is available at https://github.com/expo/eas-tests-example. 473 474## Alternative Approaches 475 476### Using development builds to speed up test run time 477 478> **Warning** This might not work properly on Android. 479 480The 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. 481 482Instead, 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). 483 484Development 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). 485 486An 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. 487 488<Collapsible summary="e2e/homeScreen.e2e.js"> 489 490```js 491/* @info New line */ 492const { openAppForDebugBuild } = require('./utils/openAppForDebugBuild'); 493/* @end */ 494 495describe('Home screen', () => { 496 beforeEach(async () => { 497 await device.launchApp({ 498 newInstance: true, 499 }); 500 /* @info New line */ await openAppForDebugBuild(); /* @end */ 501 }); 502 503 it('"Click me" button should be visible', async () => { 504 await expect(element(by.id('click-me-button'))).toBeVisible(); 505 }); 506 507 it('shows "Hi!" after tapping "Click me"', async () => { 508 await element(by.id('click-me-button')).tap(); 509 await expect(element(by.text('Hi!'))).toBeVisible(); 510 }); 511}); 512``` 513 514</Collapsible> 515 516<Collapsible summary="e2e/utils/openAppForDebugBuild.js"> 517 518```js 519const appConfig = require('../../../app.json'); 520 521module.exports.openAppForDebugBuild = async function openAppForDebugBuild() { 522 const [platform, target] = process.env.DETOX_CONFIGURATION.split('.'); 523 if (target !== 'debug') { 524 return; 525 } 526 527 await sleep(1000); 528 await device.openURL({ 529 url: process.env.EXPO_USE_UPDATES 530 ? // Testing latest published EAS update for the test_debug channel 531 getDeepLinkUrl(getLatestUpdateUrl()) 532 : // Local testing with packager 533 getDeepLinkUrl(getDevLauncherPackagerUrl(platform)), 534 }); 535 await sleep(3000); 536}; 537 538const getDeepLinkUrl = url => 539 /* @info This is project-specific, replace eastestsexample with correct value */ 540 `eastestsexample://expo-development-client/?url=${encodeURIComponent(url)}`; 541/* @end */ 542 543const getDevLauncherPackagerUrl = platform => 544 `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`; 545 546const getLatestUpdateUrl = () => 547 `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`; 548 549const getAppId = () => appConfig?.expo?.extra?.eas?.projectId ?? ''; 550 551const sleep = t => new Promise(res => setTimeout(res, t)); 552``` 553 554</Collapsible> 555