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