Editor’s note: This article was last updated 28 January 2022 to address errors and updates to React Native tools.
End-to-end testing is a technique that is widely performed in the web ecosystem using frameworks and libraries like Cypress and Puppeteer. But when it comes to mobile applications, end-to-end testing is not as common. I have a theory that most mobile developers think testing mobile applications is hard, requiring a lot of setup and configuration, therefore they skip it.
In this article, we’ll cover how to implement the end-to-end testing framework Detox in a React Native application, write several interaction tests, and, finally, integrate Detox into your development workflow. For the sake of brevity, we’ll only focus on the iOS flow. Let’s get started!
You can access the full code for this project at the GitHub repository.
Table of contents
- What is end-to-end testing?
- End-to-end testing in mobile applications
- What is Detox testing framework?
- Setting up our project
- Running React Native tests with Detox
- Improving our UI with an image carousel
- Writing Detox tests
- Running E2E tests in CI
- Conclusion
What is end-to-end testing?
In contrast to unit testing, end-to-end testing aims to cover as much of your application’s functionality as it can, simulating actual user actions. The more areas your test covers, the more reliable it will be. Therefore, an end-to-end test includes all the stages of an application: setting up your environment, installing your application, initialization, executing routines, and expecting events or behaviors to happen.
React Native end-to-end testing with Cypress
End-to-end testing using Cypress looks like the following image in the browser:
By selecting elements div
, button
, and input
, using native selectors getElementById
, getElementByName
, and getElementByClassName
, and then triggering events click
, change
, and focus
, Cypress is able to create an instance of Chrome, run a URL, and begin interacting with the webpage.
At any point in the tests, the developer can expect
something to happen or assert
something to have a specific value. If all the expectations are true
, the result of the test suite will be successful.
End-to-end testing in mobile applications
The process of testing mobile applications is actually quite similar to web applications. Let’s go through the steps:
- Create an instance of an emulator on an Android or iOS device
- Install the application
- Initialize the application
- Executing routines
- Expecting events
In a mobile app, end-to-end testing with Detox looks like the video below:
What is Detox testing framework?
Detox is an end-to-end framework for mobile apps developed by Wix, one of the top contributors to the React Native community. Wix also maintains amazing projects like react-native-navigation and react-native-ui-lib.
Detox provides great abstractions to select and trigger actions on elements. A normal test looks like the following code:
describe('Login flow', () => { it('should login successfully', async () => { await device.launchApp(); // getting the reference of an element by ID and expecting to be visible await expect(element(by.id('email'))).toBeVisible(); // Getting the reference and typing await element(by.id('email')).typeText('[email protected]'); await element(by.id('password')).typeText('123456'); // Getting the reference and executing a tap/press await element(by.text('Login')).tap(); await expect(element(by.text('Welcome'))).toBeVisible(); await expect(element(by.id('email'))).toNotExist(); }); });
As you can see, the syntax is quite readable, and by using async/await, you can write synchronous, easily-understandable tests. Let’s jump into the demo!
Setting up our project
To focus on testing over setting up React Native, I suggest bootstrapping your project using react-native init
, which creates a pretty simple and clean React Native project.
We’ll start by installing the dependency and creating a new project:
~ npm install react-native -g ~ react-native init testReactNativeDetox ###### ###### ### #### #### ### ## ### ### ## ## #### ## ## #### ## ## ## ## ## ## ### ### ## ## ######################## ## ###### ### ### ###### ### ## ## ## ## ### ### ## ### #### ### ## ### ## #### ######## #### ## ## ### ########## ### ## ## #### ######## #### ## ### ## ### #### ### ## ### ### ## ## ## ## ### ###### ### ### ###### ## ######################## ## ## ### ### ## ## ## ## ## ## #### ## ## #### ## ## ### ### ## ### #### #### ### ###### ###### Welcome to React Native! Learn Once Write Anywhere ✔ Downloading template ✔ Copying template ✔ Processing template ✔ Installing dependencies ✔ Installing CocoaPods dependencies (this may take a few minutes) Run instructions for iOS: • cd testReactNativeDetox && react-native run-ios - or - • Open testReactNativeDetox/ios/testReactNativeDetox.xcworkspace in Xcode or run "xed -b ios" • Hit the Run button Run instructions for Android: • Have an Android emulator running (quickest way to get started), or a device connected. • cd testReactNativeDetox && react-native run-android
After this step, you can try running the application in the emulator by executing the code below:
~ cd testReactNativeDetox ~ react-native run-ios
You should see an output like the following:
Running React Native tests with Detox
Before we can start testing our React Native application with Detox, you’ll need to have the following:
- Xcode installed
- Homebrew installed and updated
- Node.js installed. Use the command
brew update && brew install node
applesimutils
installed. Usebrew tap wix/brew;brew install applesimutils;
detox-cli
installed. Usenpm install -g detox-cli
Start by adding Detox as a dependency for the project:
~ yarn add detox -D
Inside the CLI, you’ll find a command that can automatically set up the project. You’ll need to run the following:
~ detox init -r jest detox[34202] INFO: [init.js] Created a file at path: e2e/config.json detox[34202] INFO: [init.js] Created a file at path: e2e/init.js detox[34202] INFO: [init.js] Created a file at path: e2e/firstTest.spec.js detox[34202] INFO: [init.js] Patching package.json at path: /Users/USERNAME/Git/testReactNativeDetox/package.json detox\[34202] INFO: [init.js] json["detox"\]["test-runner"] = "jest";
The code above will create a new folder called e2e
with a basic test and some initial configuration like init.js
, which is the file that tells Jest to start the simulator. Let’s modify the initial test to check if the two first sections are visible:
describe('Example', () => { beforeEach(async () => { await device.launchApp(); }); it('should have "Step One" section', async () => { await expect(element(by.text('Step One'))).toBeVisible(); }); it('should have "See Your Changes" section', async () => { await expect(element(by.text('See Your Changes'))).toBeVisible(); }); });
Next, you need to add configuration for Detox inside your package.json
or the .detoxrc.json
file. If you’re using package.json
, you need to add a detox
section and add all the configurations shown below. Add the following object to the detox
key, replacing the name of testReactNativeDetox
with the name of your application at every occurrence:
{ "detox": { "test-runner": "jest", "configurations": { "ios.release": { "binaryPath": "./ios/build/Build/Products/Release-iphonesimulator/testReactNativeDetox.app", "build": "xcodebuild -workspace ios/testReactNativeDetox.xcworkspace -configuration release -scheme testReactNativeDetox -sdk iphonesimulator -derivedDataPath ios/build", "type": "ios.simulator", "name": "iPhone X" } } } }
To build the application, run the following command:
~ detox build
If your build failed with the message clang:error: linker command failed with exit code 1 (use-v to see invocation)
, please refer to this solution in GitHub issues and try running the command again. Remember, you need to perform the build process above every time you make changes to your application and want to test it.
If the command above still doesn’t work, check if you replaced all occurrences of your project name in package.json
. If doing so fails as well, try opening up Xcode and building the debug version yourself.
Finally, we’ll run the test:
~ detox test PASS e2e/firstTest.spec.js (7.514s) Example âś“ should have "Step One" section (260ms) âś“ should have "See Your Changes" section (278ms)
You should receive an output like the following:
Improving our UI with an image carousel
To improve on our UI, let’s add a colorful image carousel. To save time, I decided to use an existing carousel component built by the community. For this demo, I used react-swipeable-views-native.
To generate random colors, I used randomColor.
Install both libraries as dependencies for the project:
~ yarn add react-swipeable-views-native randomcolor
Then, we’ll make a few modifications inside of our App.js
file:
import React, {Fragment} from 'react'; import { SafeAreaView, StyleSheet, ScrollView, View, Text, TextInput, Button, StatusBar, } from 'react-native'; import { Header, LearnMoreLinks, Colors, DebugInstructions, ReloadInstructions, } from 'react-native/Libraries/NewAppScreen'; import randomColor from 'randomcolor'; import SwipeableViews from 'react-swipeable-views-native'; const Slide = ({children}) => ( <View style={[ styles.slide, {backgroundColor: randomColor({luminosity: 'light'})}, ]}> {children} </View> ); const App = () => { return ( <Fragment> <StatusBar barStyle="dark-content" /> <SafeAreaView> <ScrollView contentInsetAdjustmentBehavior="automatic" style={styles.scrollView}> <Header /> {global.HermesInternal == null ? null : ( <View style={styles.engine}> <Text style={styles.footer}>Engine: Hermes</Text> </View> )} <View style={styles.body}> <SwipeableViews testID="slides"> <Slide> <Text style={styles.sectionTitle}>Step One</Text> <Text style={styles.sectionDescription}> Edit <Text style={styles.highlight}>App.js</Text> to change this screen and then come back to see your edits. </Text> </Slide> <Slide> <Text style={styles.sectionTitle}>See Your Changes</Text> <Text style={styles.sectionDescription}> <ReloadInstructions /> </Text> </Slide> <Slide> <Text style={styles.sectionTitle}>Debug</Text> <Text style={styles.sectionDescription}> <DebugInstructions /> </Text> <Button onPress={() => alert('Clicked!')} title="Click here!" /> </Slide> <Slide> <Text style={styles.sectionTitle}>Learn More</Text> <TextInput testID="docsInput" multiline style={styles.sectionDescription}> Read the docs to discover what to do next: </TextInput> </Slide> </SwipeableViews> <LearnMoreLinks /> </View> </ScrollView> </SafeAreaView> </Fragment> ); }; const styles = StyleSheet.create({ scrollView: { backgroundColor: Colors.lighter, }, engine: { position: 'absolute', right: 0, }, body: { backgroundColor: Colors.white, }, sectionTitle: { fontSize: 24, fontWeight: '600', color: Colors.black, }, sectionDescription: { marginTop: 8, fontSize: 18, fontWeight: '400', color: Colors.dark, }, highlight: { fontWeight: '700', }, footer: { color: Colors.dark, fontSize: 12, fontWeight: '600', padding: 4, paddingRight: 12, textAlign: 'right', }, slide: { padding: 24, height: 200, display: 'flex', justifyContent: 'center', }, }); export default App;
To enable swiping behavior, we wrapped all the sections inside SwipeableViews
. We then wrapped each section inside a custom View
called Slide
, which implements properties like padding
and backgroundColor
. Lastly, we added a Button
and a TextInput
component to the last two slides.
The result is as follows:
Writing Detox tests
To make things easier, let’s add two new scripts
into the package.json
:
{ "scripts": { "e2e:test": "detox test -c ios.release", "e2e:build": "detox build -c ios.release" } }
Now that the application has changed, you need to create a new build of it in order to run tests with the modified version. Execute the following command:
~ yarn e2e:build
This process may take some time. In the meantime, let’s take a quick look at the existing tests:
describe('Example', () => { beforeEach(async () => { await device.launchApp(); }); it('should show "Step One"', async () => { await expect(element(by.text('Step One'))).toBeVisible(); }); it('should show "See Your Changes"', async () => { await expect(element(by.text('See Your Changes'))).toBeVisible(); // THIS TEST WILL FAIL! }); });
The See Your Changes
section is now in the second slide of the carousel, which is not visible for the user until they swipe. Therefore, the second test will definitely fail. Let’s move Detox to that slide:
describe('Example', () => { // previous tests here it('should render "See Your Changes" in the second slide', async () => { // getting the reference of the slides and make a swipe await element(by.id('slides')).swipe('left'); await expect(element(by.text('See Your Changes'))).toBeVisible(); // no this will pass! await element(by.id('slides')).swipe('right'); }); });
We can use by.id()
because we use the testID
prop in the App
component. At this point, you can execute the end-to-end tests with the following command, and they should pass:
~ yarn e2e:test PASS e2e/firstTest.spec.js (7.514s) Example âś“ should have "Step One" section (260ms) âś“ should render "See Your Changes" in the second slide (993ms)
Let’s add a few more tests to cover the following scenarios:
- Test that the carousel allows the user to move back and forth inside the slides
- Move the third slide and interact with the
Button
- Move the last slide and interact with the
TextInput
describe('Example', () => { // previous tests here it('should enable swiping back and forth', async () => { await expect(element(by.text('Step One'))).toBeVisible(); await element(by.id('slides')).swipe('left'); await element(by.id('slides')).swipe('right'); await expect(element(by.text('Step One'))).toBeVisible(); }); it('should render "Debug" and have a Button to click in the third slide', async () => { await element(by.id('slides')).swipe('left'); await element(by.id('slides')).swipe('left'); await expect(element(by.text('Debug'))).toBeVisible(); await element(by.text('Click here!')).tap(); await expect(element(by.text('Clicked!'))).toBeVisible(); await element(by.text('OK')).tap(); }); it('should render "Learn More" and change text in the fourth slide', async () => { await element(by.id('slides')).swipe('left'); await element(by.id('slides')).swipe('left'); await element(by.id('slides')).swipe('left'); await expect(element(by.text('Learn More'))).toBeVisible(); const docsInput = element(by.id('docsInput')); await expect(docsInput).toBeVisible(); await docsInput.clearText(); await docsInput.typeText('Maybe later!'); await expect(docsInput).toHaveText('Maybe later!'); }); });
Let’s run the tests again:
~ yarn e2e:test PASS e2e/firstTest.spec.js (22.128s) Example âś“ should have "Step One" section (268ms) âś“ should render "See Your Changes" in the second slide (982ms) âś“ should enable swiping back and forth (1861ms) âś“ should render "Debug" and have a Button to click in the third slide (2710ms) âś“ should render "Learn More" and change text in the fourth slide (9964ms)
Your app should now look like the following:
Running E2E test in CI
Running tests inside CI is important, basically eliminating the need for manual testing and preventing shipping bugs to production. For this example, I decided to use TravisCI because it has an amazing integration with GitHub and also provides an unlimited plan for open source projects.
If you’re using GitHub, you can install the Travis application, create a new plan, and allow it to access your repositories. After that, you need to create a new file inside your project called .travis.yml
, which defines the steps you want to run in CI.
I tweaked the CI configuration inside the official Detox docs slightly. Below is the configuration that worked in my case:
language: objective-c osx_image: xcode10.2 branches: only: - master env: global: - NODE_VERSION=stable install: - brew tap wix/brew - brew install applesimutils - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash - export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install $NODE_VERSION - nvm use $NODE_VERSION - nvm alias default $NODE_VERSION - npm install -g react-native-cli - npm install -g detox-cli - npm install - cd ios; pod install; cd -; script: - npm run e2e:ci
Finally, add the command e2e:ci
into your package.json
. The detox build
command will build the application, detox test
will run the tests, and the --cleanup
flag will close the emulator to finish the execution:
{ "scripts": { "e2e:test": "detox test -c ios.release", "e2e:build": "detox build -c ios.release", "e2e:ci": "npm run e2e:build && npm run e2e:test -- --cleanup" } }
Once you’ve pushed all the changes into your master
branch, try to open a new pull request. You should see a new pull request checker has been added, which we’ll call Travis, which runs the Detox tests:
You can access the full log in Travis for that pull request.
Conclusion
I highly encourage you try Detox for testing your React Native applications. Detox is an amazing end-to-end testing solution for mobile. After using it for quite some time, I’d say it has the following pros and cons:
- Very well abstracted syntax for matchers and to trigger specific actions
- Great integration with Jest
- Ability to run tests in CI
Sometimes, you might encounter configuration errors, and finding the proper solution may take some time. The best way to address this problem is to go and take a deep look at GitHub Issues. I hope you enjoyed this article, happy coding!
LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Pretty good intro to E2E with Detox. One thing I noticed was that I needed to run “applesimutils –list” on the console to get a valid name for my device to replace “iPhone X” with “iPhone 11” since that is the emulator I’m currently running. Might be a good idea to add that as a note to this article.
Can I test using a real device?