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