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
13> **info** **Android not yet supported**: EAS Build does not yet support running tests on an Android Emulator — this will be coming soon.
14
15With 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.
16
17This 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.
18
19## Running iOS tests
20
21### 1. Initialize a new Bare Workflow project
22
23Let's start by initializing a new Expo project and running `npx expo prebuild` to generate the native projects.
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    '# Generate native code',
32    '$ npx expo prebuild --platform ios',
33  ]}
34/>
35
36### 2. Make home screen interactive
37
38The 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.
39Later, we're going to write a test that's going to tap the button and check whether the text has been displayed.
40
41<div style={{ display: 'flex', justifyContent: 'center' }}>
42  <img src="/static/images/eas-build/tests/01-click-me.png" style={{ maxWidth: '45%' }} />
43  <img src="/static/images/eas-build/tests/02-hi.png" style={{ maxWidth: '45%' }} />
44</div>
45
46<Collapsible summary="�� See the source code">
47
48```js App.js
49import { StatusBar } from 'expo-status-bar';
50import { useState } from 'react';
51import { Pressable, StyleSheet, Text, View } from 'react-native';
52
53export default function App() {
54  const [clicked, setClicked] = useState(false);
55
56  return (
57    <View style={styles.container}>
58      {!clicked && (
59        <Pressable testID="click-me-button" style={styles.button} onPress={() => setClicked(true)}>
60          <Text style={styles.text}>Click me</Text>
61        </Pressable>
62      )}
63      {clicked && <Text style={styles.hi}>Hi!</Text>}
64      <StatusBar style="auto" />
65    </View>
66  );
67}
68
69const styles = StyleSheet.create({
70  container: {
71    flex: 1,
72    backgroundColor: '#fff',
73    alignItems: 'center',
74    justifyContent: 'center',
75  },
76  hi: {
77    fontSize: 30,
78    color: '#4630EB',
79  },
80  button: {
81    alignItems: 'center',
82    justifyContent: 'center',
83    paddingVertical: 12,
84    paddingHorizontal: 32,
85    borderRadius: 4,
86    elevation: 3,
87    backgroundColor: '#4630EB',
88  },
89  text: {
90    fontSize: 16,
91    lineHeight: 21,
92    fontWeight: 'bold',
93    letterSpacing: 0.25,
94    color: 'white',
95  },
96});
97```
98
99</Collapsible>
100
101### 3. Set up Detox
102
103#### Install dependencies
104
105Let'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.
106
107<Terminal
108  cmd={[
109    '# Install jest & detox',
110    '$ npm install --save-dev jest detox',
111    '# Create Detox configuration files',
112    '$ npx detox init -r jest',
113  ]}
114/>
115
116> 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.
117
118#### Configure Detox
119
120Detox 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 (using `npx detox build --configuration ios`).
121
122Edit **.detoxrc.json** and replace the `ios` configuration with:
123
124```json .detoxrc.json
125{
126  "apps": {
127    "ios": {
128      "type": "ios.app",
129      // note: replace "eastestsexample" with your project's name
130      "build": "xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build",
131      // note: replace "eastestsexample" with your project's name
132      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app"
133    }
134  }
135}
136```
137
138### 4. Write E2E tests
139
140Next, 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:
141
142```js homeScreen.e2e.js
143describe('Home screen', () => {
144  beforeAll(async () => {
145    await device.launchApp();
146  });
147
148  beforeEach(async () => {
149    await device.reloadReactNative();
150  });
151
152  it('"Click me" button should be visible', async () => {
153    await expect(element(by.id('click-me-button'))).toBeVisible();
154  });
155
156  it('shows "Hi!" after tapping "Click me"', async () => {
157    await element(by.id('click-me-button')).tap();
158    await expect(element(by.text('Hi!'))).toBeVisible();
159  });
160});
161```
162
163There are two tests in the suite:
164
165- One that checks whether the "Click me" button is visible on the home screen.
166- Another that verifies that tapping the button triggers displaying "Hi!".
167
168Both tests assume the button has the `testID` set to `click-me-button`. See [the source code](#1-make-home-screen-interactive) for details.
169
170### 5. Configure EAS Build
171
172Now that we have configured Detox and written our first E2E test, let's configure EAS Build and run the tests in the cloud.
173
174#### Create eas.json
175
176The following command creates [eas.json](/build/eas-json.mdx) in the project's root directory:
177
178<Terminal cmd={['$ eas build:configure']} />
179
180#### Configure EAS Build
181
182There are three more steps to configure EAS Build for running E2E tests as part of the build:
183
184- Detox tests are run in the iOS simulator. We will define a build profile that builds your app for simulator.
185- Install the [`applesimutils`](https://github.com/wix/AppleSimulatorUtils) command line util required by Detox.
186- Configure EAS Build to run Detox tests after successfully building the app.
187
188Edit **eas.json** and add the `test` build profile:
189
190```json eas.json
191{
192  "build": {
193    "test": {
194      "ios": {
195        "simulator": true
196      }
197    }
198  }
199}
200```
201
202Create **eas-hooks/eas-build-pre-install.sh** that will be used to install `applesimutils` with `brew`:
203
204```sh eas-hooks/eas-build-pre-install.sh
205#!/usr/bin/env bash
206
207set -eox pipefail
208
209if [[ "$EAS_BUILD_PLATFORM" == "ios" && "$EAS_BUILD_PROFILE" == "test" ]]; then
210  brew tap wix/brew
211  brew install applesimutils
212fi
213```
214
215Next, create **eas-hooks/eas-build-on-success.sh** with the following contents. The script runs Detox tests in headless mode:
216
217```sh eas-hooks/eas-build-on-success.sh
218#!/usr/bin/env bash
219
220set -eox pipefail
221
222if [[ "$EAS_BUILD_PLATFORM" == "ios" && "$EAS_BUILD_PROFILE" == "test" ]]; then
223  detox test --configuration ios --headless
224fi
225```
226
227Edit **package.json** to use [EAS Build hooks](/build-reference/npm-hooks.mdx) to run the above scripts on EAS Build:
228
229```json package.json
230{
231  "scripts": {
232    "eas-build-pre-install": "./eas-hooks/eas-build-pre-install.sh",
233    "eas-build-on-success": "./eas-hooks/eas-build-on-success.sh"
234  }
235}
236```
237
238> 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`.
239
240### 6. Run tests on EAS Build
241
242Running the tests on EAS Build is like running a regular build:
243
244<Terminal cmd={['$ eas build -p ios --profile test']} />
245
246If you have set up everything correctly you should see the successful test result in the build logs:
247
248<ImageSpotlight src="/static/images/eas-build/tests/03-logs.png" style={{ maxWidth: '90%' }} />
249
250### 7. Upload screenshots of failed test cases
251
252> This step is optional but highly recommended.
253
254When 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**.
255
256#### Take screenshots for failed tests
257
258[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.
259
260Neither `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.
261
262Edit **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:
263
264```ts e2e/environment.js
265class CustomDetoxEnvironment extends DetoxCircusEnvironment {
266  // ...
267
268  async handleTestEvent(event, state) {
269    const { name } = event;
270
271    if (['test_start', 'test_fn_start'].includes(name)) {
272      this.global.testFailed = false;
273    }
274
275    if (name === 'test_fn_failure') {
276      this.global.testFailed = true;
277    }
278
279    await super.handleTestEvent(event, state);
280  }
281}
282```
283
284After modifying the environment class, define an `afterEach` hook that calls `device.takeScreenshot()` for failed tests. Create the **e2e/setup.ts** file with the following snippet:
285
286```ts e2e/setup.ts
287afterEach(async () => {
288  if (testFailed) {
289    await device.takeScreenshot('screenshot');
290  }
291});
292```
293
294The last step is to configure `jest` to load `e2e/setup.ts` before running tests. This way, you don't need to include the `afterEach` hook in every test suite:
295
296```json e2e/config.json
297{
298  /// ...
299  "setupFilesAfterEnv": ["./setup.js"]
300}
301```
302
303After making those changes, screenshots for failed tests will be saved in the `artifacts` directory.
304
305#### Configure EAS Build for screenshots upload
306
307Edit **eas.json** and add `buildArtifactPaths` to the `test` build profile:
308
309```json eas.json
310{
311  "build": {
312    "test": {
313      "ios": {
314        "simulator": true,
315        "buildArtifactPaths": ["artifacts/**/*.png"]
316      }
317    }
318  }
319}
320```
321
322In 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.
323
324If you run E2E tests locally, remember to add `artifacts` to `.gitignore`:
325
326```stylus .gitignore
327artifacts/
328```
329
330#### Break a test and run a build
331
332To test the new configuration, let's break a test and see that EAS Build uploads the screenshots.
333
334Edit **e2e/homeScreen.e2e.js** and make the following change:
335
336<DiffBlock
337  raw={`
338diff --git a/e2e/homeScreen.e2e.js b/e2e/homeScreen.e2e.js
339index e28acfa..c65e90c 100644
340--- a/e2e/homeScreen.e2e.js
341+++ b/e2e/homeScreen.e2e.js
342@@ -8,7 +8,7 @@ describe('Home screen', () => {
343   });
344
345   it('"Click me" button should be visible', async () => {
346-    await expect(element(by.id('click-me-button'))).toBeVisible();
347+    await expect(element(by.id('click-me-button'))).not.toBeVisible();
348   });
349
350   it('shows "Hi!" after tapping "Click me"', async () => {
351`} />
352
353Run a build with the following command and wait for it to finish:
354
355<Terminal cmd={['eas build -p ios --profile test']} />
356
357After going to the build details page you should see that the build failed. Use the **"Download artifacts"** button to download and examine the screenshot:
358
359<ImageSpotlight src="/static/images/eas-build/tests/04-artifacts.png" style={{ maxWidth: '90%' }} />
360
361## Repository
362
363The full example from this guide is available at https://github.com/expo/eas-tests-example.
364
365## Alternative Approaches
366
367### Using development builds to speed up test run time
368
369The 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.
370
371Instead, 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).
372
373Development 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).
374
375An example of such a Detox test is shown below. Full example code is available on the [eas-tests-example repository](https://github.com/expo/eas-tests-example).
376
377<Collapsible summary="homeScreen.e2e.js">
378
379```js
380const {
381  sleepAsync,
382  getConfigurationName,
383  // getDevLauncherPackagerUrl,
384  getLatestUpdateUrl,
385  getDeepLinkUrl,
386} = require('./utils');
387
388describe('Home screen', () => {
389  beforeEach(async () => {
390    await device.launchApp({
391      newInstance: true,
392    });
393    if (getConfigurationName().indexOf('debug') !== -1) {
394      await device.openURL({
395        // Local testing with packager
396        //url: getDeepLinkUrl(getDevLauncherPackagerUrl(platform)),
397        // Testing latest published EAS update for the test_debug channel
398        url: getDeepLinkUrl(getLatestUpdateUrl()),
399      });
400    }
401    await sleepAsync(3000);
402  });
403
404  it('"Click me" button should be visible', async () => {
405    await expect(element(by.id('click-me-button'))).toBeVisible();
406  });
407
408  it('shows "Hi!" after tapping "Click me"', async () => {
409    await element(by.id('click-me-button')).tap();
410    await expect(element(by.text('Hi!'))).toBeVisible();
411  });
412});
413```
414
415</Collapsible>
416
417<Collapsible summary="utils.js">
418
419```js
420const { exec } = require('child_process');
421const argparse = require('detox/src/utils/argparse');
422const appConfig = require('../app.json');
423
424/**
425 * General util methods
426 */
427
428/**
429 * Async wait for n milliseconds
430 */
431const sleepAsync = t => new Promise(res => setTimeout(res, t));
432
433/**
434 * Get Detox configuration being used
435 */
436const getConfigurationName = () => argparse.getArgValue('configuration');
437
438/**
439 * Get EAS app ID
440 */
441const getAppId = () => appConfig?.expo?.extra?.eas?.projectId || '';
442
443/**
444 * Methods to construct test URLs
445 */
446
447/**
448 * For local dev testing, returns the packager URL with the disable onboarding query param
449 */
450const getDevLauncherPackagerUrl = platform => {
451  return `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`;
452};
453
454/**
455 * Returns the URL for the most recent update for the 'test_debug' EAS update channel
456 */
457const getLatestUpdateUrl = () =>
458  `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`;
459
460/**
461 * Wraps a React Native bundle URL in the deep link form required by expo-dev-launcher for this app
462 */
463const getDeepLinkUrl = url =>
464  `eastestsexample://expo-development-client/?url=${encodeURIComponent(url)}`;
465
466module.exports = {
467  sleepAsync,
468  getConfigurationName,
469  getAppId,
470  getDevLauncherPackagerUrl,
471  getLatestUpdateUrl,
472  getDeepLinkUrl,
473  invokeDevLauncherUrl,
474};
475```
476
477</Collapsible>
478