1---
2title: Run E2E tests on EAS Build
3sidebar_title: Run E2E tests
4description: Learn how to set up and run E2E tests on EAS Build with popular libraries such as Detox.
5---
6
7import { Collapsible } from '~/ui/components/Collapsible';
8import { Terminal, DiffBlock } from '~/ui/components/Snippet';
9import ImageSpotlight from '~/components/plugins/ImageSpotlight';
10
11> **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,
12> 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.
13
14With 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.
15
16This 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 to do so.
17
18## Running iOS tests
19
20### 1. Initialize a new bare workflow project
21
22Let's start by initializing a new Expo project, installing and configuring `@config-plugins/detox`, and running `npx expo prebuild` to generate the native projects.
23
24Start with the following commands:
25
26<Terminal
27  cmd={[
28    '# Initialize a new project',
29    '$ npx create-expo-app eas-tests-example',
30    '# cd into the project directory',
31    '$ cd eas-tests-example',
32    '# Install @config-plugins/detox',
33    '$ npm install --save-dev @config-plugins/detox',
34  ]}
35/>
36
37Now, 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.
38
39```json app.json
40{
41  "expo": {
42    // ...
43    "plugins": ["@config-plugins/detox"]
44  }
45}
46```
47
48Run prebuild to generate the native projects:
49
50<Terminal cmd={['$ npx expo prebuild']} />
51
52### 2. Make home screen interactive
53
54The 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.
55Later, we're going to write a test that's going to tap the button and check whether the text has been displayed.
56
57<div style={{ display: 'flex', justifyContent: 'center' }}>
58  <img src="/static/images/eas-build/tests/01-click-me.png" style={{ maxWidth: '45%' }} />
59  <img src="/static/images/eas-build/tests/02-hi.png" style={{ maxWidth: '45%' }} />
60</div>
61
62<Collapsible summary="�� See the source code">
63
64```js App.js
65import { StatusBar } from 'expo-status-bar';
66import { useState } from 'react';
67import { Pressable, StyleSheet, Text, View } from 'react-native';
68
69export default function App() {
70  const [clicked, setClicked] = useState(false);
71
72  return (
73    <View style={styles.container}>
74      {!clicked && (
75        <Pressable testID="click-me-button" style={styles.button} onPress={() => setClicked(true)}>
76          <Text style={styles.text}>Click me</Text>
77        </Pressable>
78      )}
79      {clicked && <Text style={styles.hi}>Hi!</Text>}
80      <StatusBar style="auto" />
81    </View>
82  );
83}
84
85const styles = StyleSheet.create({
86  container: {
87    flex: 1,
88    backgroundColor: '#fff',
89    alignItems: 'center',
90    justifyContent: 'center',
91  },
92  hi: {
93    fontSize: 30,
94    color: '#4630EB',
95  },
96  button: {
97    alignItems: 'center',
98    justifyContent: 'center',
99    paddingVertical: 12,
100    paddingHorizontal: 32,
101    borderRadius: 4,
102    elevation: 3,
103    backgroundColor: '#4630EB',
104  },
105  text: {
106    fontSize: 16,
107    lineHeight: 21,
108    fontWeight: 'bold',
109    letterSpacing: 0.25,
110    color: 'white',
111  },
112});
113```
114
115</Collapsible>
116
117### 3. Set up Detox
118
119#### Install dependencies
120
121Let'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.
122
123<Terminal
124  cmd={[
125    '# Install jest and detox',
126    '$ npm install --save-dev jest detox',
127    '# Create Detox configuration files',
128    '$ npx detox init -r jest',
129  ]}
130/>
131
132> 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.
133
134#### Configure Detox
135
136Detox 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`).
137
138Edit **detox.config.js** and replace the configuration with:
139
140```js detox.config.js
141/** @type {Detox.DetoxConfig} */
142module.exports = {
143  logger: {
144    level: process.env.CI ? 'debug' : undefined,
145  },
146  testRunner: {
147    $0: 'jest',
148    args: {
149      config: 'e2e/jest.config.js',
150      _: ['e2e'],
151    },
152  },
153  artifacts: {
154    plugins: {
155      log: process.env.CI ? 'failing' : undefined,
156      /* @info In Detox 20, this plugin setting will take screenshots if tests fail */
157      screenshot: 'failing',
158      /* @end */
159    },
160  },
161  apps: {
162    'ios.release': {
163      type: 'ios.app',
164      /* @info This is project-specific, replace eastestsexample with correct value */
165      build:
166        'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
167      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app',
168      /* @end */
169    },
170    'android.release': {
171      type: 'android.apk',
172      build:
173        'cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..',
174      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
175    },
176  },
177  devices: {
178    simulator: {
179      type: 'ios.simulator',
180      device: {
181        type: 'iPhone 14',
182      },
183    },
184    emulator: {
185      type: 'android.emulator',
186      device: {
187        avdName: 'pixel_4',
188      },
189    },
190  },
191  configurations: {
192    'ios.release': {
193      device: 'simulator',
194      app: 'ios.release',
195    },
196    'android.release': {
197      device: 'emulator',
198      app: 'android.release',
199    },
200  },
201};
202```
203
204### 4. Write E2E tests
205
206Next, 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:
207
208```js e2e/homeScreen.e2e.js
209describe('Home screen', () => {
210  beforeAll(async () => {
211    await device.launchApp();
212  });
213
214  beforeEach(async () => {
215    await device.reloadReactNative();
216  });
217
218  it('"Click me" button should be visible', async () => {
219    await expect(element(by.id('click-me-button'))).toBeVisible();
220  });
221
222  it('shows "Hi!" after tapping "Click me"', async () => {
223    await element(by.id('click-me-button')).tap();
224    await expect(element(by.text('Hi!'))).toBeVisible();
225  });
226});
227```
228
229There are two tests in the suite:
230
231- One that checks whether the "Click me" button is visible on the home screen.
232- Another that verifies that tapping the button triggers displaying "Hi!".
233
234Both tests assume the button has the `testID` set to `click-me-button`. See [the source code](#2-make-home-screen-interactive) for details.
235
236### 5. Configure EAS Build
237
238Now that we have configured Detox and written our first E2E test, let's configure EAS Build and run the tests in the cloud.
239
240#### Create eas.json
241
242The following command creates [eas.json](/build/eas-json.mdx) in the project's root directory:
243
244<Terminal cmd={['$ eas build:configure']} />
245
246#### Configure EAS Build
247
248There are a few more steps to configure EAS Build for running E2E tests as part of the build:
249
250- Android tests:
251  - Tests are run in the Android Emulator. You will define a build profile that builds your app for the emulator (produces an `apk` file).
252  - Install the emulator and all its system dependencies.
253- iOS test:
254  - Tests are run in the iOS Simulator. You will define a build profile that builds your app for the simulator.
255  - Install the [`applesimutils`](https://github.com/wix/AppleSimulatorUtils) command line util.
256- Configure EAS Build to run Detox tests after successfully building the app.
257
258Edit **eas.json** and add the `test` build profile:
259
260```json eas.json
261{
262  "build": {
263    "test": {
264      "android": {
265        "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
266        "withoutCredentials": true
267      },
268      "ios": {
269        "simulator": true
270      }
271    }
272  }
273}
274```
275
276Create **eas-hooks/eas-build-pre-install.sh** that installs the necessary tools and dependencies for the given platform:
277
278```sh eas-hooks/eas-build-pre-install.sh
279#!/usr/bin/env bash
280
281set -eox pipefail
282
283if [[ "$EAS_BUILD_RUNNER" == "eas-build" && "$EAS_BUILD_PROFILE" == "test"* ]]; then
284  if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
285    sudo apt-get --quiet update --yes
286
287    # Install emulator & video bridge dependencies
288    # Source: https://github.com/react-native-community/docker-android/blob/master/Dockerfile
289    sudo apt-get --quiet install --yes \
290      libc6 \
291      libdbus-1-3 \
292      libfontconfig1 \
293      libgcc1 \
294      libpulse0 \
295      libtinfo5 \
296      libx11-6 \
297      libxcb1 \
298      libxdamage1 \
299      libnss3 \
300      libxcomposite1 \
301      libxcursor1 \
302      libxi6 \
303      libxext6 \
304      libxfixes3 \
305      zlib1g \
306      libgl1 \
307      pulseaudio \
308      socat
309
310    # Emulator must be API 31 -- API 32 and 33 fail due to https://github.com/wix/Detox/issues/3762
311    sdkmanager --install "system-images;android-31;google_apis;x86_64"
312    avdmanager --verbose create avd --force --name "pixel_4" --device "pixel_4" --package "system-images;android-31;google_apis;x86_64"
313  else
314    brew tap wix/brew
315    brew install applesimutils
316  fi
317fi
318
319```
320
321Next, 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 before 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.
322
323```sh eas-hooks/eas-build-on-success.sh
324#!/usr/bin/env bash
325
326function cleanup()
327{
328  echo 'Cleaning up...'
329  if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
330    # Kill emulator
331    adb emu kill &
332  fi
333}
334
335if [[ "$EAS_BUILD_PROFILE" != "test" ]]; then
336  exit
337fi
338
339# Fail if anything errors
340set -eox pipefail
341# If this script exits, trap it first and clean up the emulator
342trap cleanup EXIT
343
344ANDROID_EMULATOR=pixel_4
345
346if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
347  # Start emulator
348  $ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null &
349
350  # Wait for emulator
351  max_retry=10
352  counter=0
353  until adb shell getprop sys.boot_completed; do
354    sleep 10
355    [[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1
356    counter=$((counter + 1))
357  done
358
359  # Execute Android tests
360  if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
361    detox test --configuration android.release
362  fi
363else
364  # Execute iOS tests
365  if [[  "$EAS_BUILD_PROFILE" == "test" ]]; then
366    detox test --configuration ios.release
367  fi
368fi
369```
370
371Edit **package.json** to use [EAS Build hooks](/build-reference/npm-hooks.mdx) to run the above scripts on EAS Build:
372
373```json package.json
374{
375  "scripts": {
376    "eas-build-pre-install": "./eas-hooks/eas-build-pre-install.sh",
377    "eas-build-on-success": "./eas-hooks/eas-build-on-success.sh"
378  }
379}
380```
381
382> 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`.
383
384### 6. Run tests on EAS Build
385
386Running the tests on EAS Build is like running a regular build:
387
388<Terminal cmd={['$ eas build -p all -e test']} />
389
390If you have set up everything correctly you should see the successful test result in the build logs:
391
392<ImageSpotlight src="/static/images/eas-build/tests/03-logs.png" style={{ maxWidth: '90%' }} />
393
394### 7. Upload screenshots of failed test cases
395
396> This step is optional but highly recommended.
397
398When 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**.
399
400#### Take screenshots for failed tests
401
402Detox supports taking in-test screenshots of the device. The [**detox.config.js** sample](/build-reference/e2e-tests/#configure-detox) above includes a line to configure Detox to take screenshots of failed tests.
403
404#### Configure EAS Build for screenshots upload
405
406Edit **eas.json** and add `buildArtifactPaths` to the `test` build profile:
407
408```json eas.json
409{
410  "build": {
411    "test": {
412      "android": {
413        "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
414        "withoutCredentials": true
415      },
416      "ios": {
417        "simulator": true
418      },
419      /* @info */
420      "buildArtifactPaths": ["artifacts/**/*.png"]
421      /* @end */
422    }
423  }
424}
425```
426
427In 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.
428
429If you run E2E tests locally, remember to add `artifacts` to `.gitignore`:
430
431```stylus .gitignore
432artifacts/
433```
434
435#### Break a test and run a build
436
437To test the new configuration, let's break a test and see that EAS Build uploads the screenshots.
438
439Edit **e2e/homeScreen.e2e.js** and make the following change:
440
441<DiffBlock source="/static/diffs/e2e-tests-homescreen.diff" />
442
443Run an iOS build with the following command and wait for it to finish:
444
445<Terminal cmd={['$ eas build -p ios -e test']} />
446
447After going to the build details page you should see that the build failed. Use the **"Download artifacts"** button to download and examine the screenshot:
448
449<ImageSpotlight src="/static/images/eas-build/tests/04-artifacts.png" style={{ maxWidth: '90%' }} />
450
451## Repository
452
453The full example from this guide is available at https://github.com/expo/eas-tests-example.
454
455## Alternative approaches
456
457### Using development builds to speed up test run time
458
459The 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.
460
461Instead, we can use [development builds](/develop/development-builds/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](/develop/development-builds/development-workflows/#deep-linking-urls).
462
463Development 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).
464
465An 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.
466
467<Collapsible summary="e2e/homeScreen.e2e.js">
468
469```js
470/* @info New line */
471const { openApp } = require('./utils/openApp');
472/* @end */
473
474describe('Home screen', () => {
475  beforeEach(async () => {
476    /* @info New line */ await openApp(); /* @end */
477  });
478
479  it('"Click me" button should be visible', async () => {
480    await expect(element(by.id('click-me-button'))).toBeVisible();
481  });
482
483  it('shows "Hi!" after tapping "Click me"', async () => {
484    await element(by.id('click-me-button')).tap();
485    await expect(element(by.text('Hi!'))).toBeVisible();
486  });
487});
488```
489
490</Collapsible>
491
492<Collapsible summary="e2e/utils/openApp.js (new file)">
493
494```js
495const appConfig = require('../../../app.json');
496const { resolveConfig } = require('detox/internals');
497
498const platform = device.getPlatform();
499
500module.exports.openApp = async function openApp() {
501  const config = await resolveConfig();
502  if (config.configurationName.split('.')[1] === 'debug') {
503    return await openAppForDebugBuild(platform);
504  } else {
505    return await device.launchApp({
506      newInstance: true,
507    });
508  }
509};
510
511async function openAppForDebugBuild(platform) {
512  const deepLinkUrl = process.env.EXPO_USE_UPDATES
513    ? // Testing latest published EAS update for the test_debug channel
514      getDeepLinkUrl(getLatestUpdateUrl())
515    : // Local testing with packager
516      getDeepLinkUrl(getDevLauncherPackagerUrl(platform));
517
518  if (platform === 'ios') {
519    await device.launchApp({
520      newInstance: true,
521    });
522    sleep(3000);
523    await device.openURL({
524      url: deepLinkUrl,
525    });
526  } else {
527    await device.launchApp({
528      newInstance: true,
529      url: deepLinkUrl,
530    });
531  }
532
533  await sleep(3000);
534}
535
536const getDeepLinkUrl = url =>
537  `eastestsexample://expo-development-client/?url=${encodeURIComponent(url)}`;
538
539const getDevLauncherPackagerUrl = platform =>
540  `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`;
541
542const getLatestUpdateUrl = () =>
543  `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`;
544
545const getAppId = () => appConfig?.expo?.extra?.eas?.projectId ?? '';
546
547const sleep = t => new Promise(res => setTimeout(res, t));
548```
549
550</Collapsible>
551
552<Collapsible summary="detox.config.js">
553
554```js
555/** @type {Detox.DetoxConfig} */
556module.exports = {
557  logger: {
558    level: process.env.CI ? 'debug' : undefined,
559  },
560  testRunner: {
561    $0: 'jest',
562    args: {
563      config: 'e2e/jest.config.js',
564      _: ['e2e'],
565    },
566  },
567  artifacts: {
568    plugins: {
569      log: process.env.CI ? 'failing' : undefined,
570      screenshot: 'failing',
571    },
572  },
573  apps: {
574    'ios.debug': {
575      type: 'ios.app',
576      build:
577        'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Debug -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
578      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/eastestsexample.app',
579    },
580    'ios.release': {
581      type: 'ios.app',
582      build:
583        'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
584      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app',
585    },
586    'android.debug': {
587      type: 'android.apk',
588      build:
589        'cd android && ./gradlew :app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug && cd ..',
590      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
591    },
592    'android.release': {
593      type: 'android.apk',
594      build:
595        'cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..',
596      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
597    },
598  },
599  devices: {
600    simulator: {
601      type: 'ios.simulator',
602      device: {
603        type: 'iPhone 14',
604      },
605    },
606    emulator: {
607      type: 'android.emulator',
608      device: {
609        avdName: 'pixel_4',
610      },
611    },
612  },
613  configurations: {
614    'ios.debug': {
615      device: 'simulator',
616      app: 'ios.debug',
617    },
618    'ios.release': {
619      device: 'simulator',
620      app: 'ios.release',
621    },
622    'android.debug': {
623      device: 'emulator',
624      app: 'android.debug',
625    },
626    'android.release': {
627      device: 'emulator',
628      app: 'android.release',
629    },
630  },
631};
632```
633
634</Collapsible>
635
636<Collapsible summary="eas-hooks/eas-build-on-success.sh">
637
638```sh
639#!/usr/bin/env bash
640
641function cleanup()
642{
643  echo 'Cleaning up...'
644  if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
645    # Kill emulator
646    adb emu kill &
647  fi
648}
649
650if [[ "$EAS_BUILD_PROFILE" != "test"* ]]; then
651  exit
652fi
653
654# Fail if anything errors
655set -eox pipefail
656# If this script exits, trap it first and clean up the emulator
657trap cleanup EXIT
658
659ANDROID_EMULATOR=pixel_4
660
661if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
662  # Start emulator
663  $ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null &
664
665  # Wait for emulator
666  max_retry=10
667  counter=0
668  until adb shell getprop sys.boot_completed; do
669    sleep 10
670    [[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1
671    counter=$((counter + 1))
672  done
673
674
675  # Execute Android tests
676  if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
677    detox test --configuration android.release
678  fi
679  if [[ "$EAS_BUILD_PROFILE" == "test_debug" ]]; then
680    detox test --configuration android.debug
681  fi
682else
683  # Execute iOS tests
684  if [[  "$EAS_BUILD_PROFILE" == "test" ]]; then
685    detox test --configuration ios.release
686  fi
687  if [[ "$EAS_BUILD_PROFILE" == "test_debug" ]]; then
688    detox test --configuration ios.debug
689  fi
690fi
691```
692
693</Collapsible>
694
695<Collapsible summary="eas.json">
696
697```json
698{
699  "build": {
700    "test": {
701      "android": {
702        "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
703        "withoutCredentials": true
704      },
705      "ios": {
706        "simulator": true
707      }
708    },
709    /* @info New section */ "test_debug": {
710      "android": {
711        "gradleCommand": ":app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug",
712        "withoutCredentials": true
713      },
714      "ios": {
715        "buildConfiguration": "Debug",
716        "simulator": true
717      },
718      "env": {
719        "EXPO_USE_UPDATES": "1"
720      },
721      "channel": "test_debug"
722    } /* @end */
723  }
724}
725```
726
727</Collapsible>
728