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