Written by Emanuel Suriano✏️
End-to-end testing is a technique that is widely performed in the web ecosystem with frameworks like Cypress, Puppeteer, or maybe with your own custom implementation.
But when it comes to the mobile world, this practice is not that common, and there are several existing solutions to address. I have a theory that most mobile developers think testing mobile application is hard and requires a lot of setup and configuration, and therefore they just skip it.
The goal of this article is to explain how to implement the end-to-end testing framework Detox in a React Native application, write a bunch of interaction tests, and, finally, integrate it into your development workflow.
⚠Disclaimer⚠: Given the length of this article, I will only focus on the iOS flow. Nevertheless, I plan to release a second part covering Android, too.
A quick introduction to end-to-end testing 📖
Let’s start with a definition of end-to-end from Software Testing Dictionary:
End-to-end testing is a technique used to test whether the flow of an application right from start to finish is behaving as expected. The purpose of performing end-to-end testing is to identify system dependencies and to ensure that data integrity is maintained between various system components and systems.
In contrast to unit testing, end-to-end testing tries to cover as much of your application’s functionality as it can. The more it covers, the more reliable your tests will be. Therefore, it includes all the stages of an application:
- Set up environment
- Installation of the application (if it’s necessary)
- Initialization
- Executing routines
- Expecting events or behaviors to happen
This is how end-to-end testing looks in the browser using Cypress:
Cypress is able to create an instance of Chrome, run a URL, and then start interacting with the webpage by selecting elements (div
, button
, input
) using native selectors (getElementById
, getElementByName
, getElementByClassName
), and then triggering events (click
, change
, focus
).
At any point in the tests, the developer can assert
/expect
something to happen or to have a specific value. In case all the expectations were true, the result of the test suite will be successful.
End-to-end testing in mobile 🤯
The process of testing mobile applications is actually quite similar to the web. Let’s go thought the previously described steps:
- Set up environment: create an instance of an emulator (Android/iOS device)
- Installation: install the application
- Initialization: run the application
-
Executing routines: Depending on the framework this may change, but all of them are using native directives to get the reference of an element (
Button
,View
,TextInput
) and then executing actions (press
,type
,focus
) -
Expecting events: Using the same functions described before, they can
assert
/expect
values or events that happened
This is how end-to-end testing looks like in mobile using Detox:
What is Detox, and why should you pick it? ⭐
Detox is an end-to-end framework for mobile apps developed by Wix, one of the top contributors inside the React Native community. They also maintain amazing projects such as react-native-navigation, react-native-ui-lib, and, of course, Detox.
What I like about this framework is the great abstraction it provides to select and trigger actions on elements. This is how a normal test looks:
describe('Login flow', () => {
it('should login successfully', async () => {
await device.reloadReactNative();
// 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('john@example.com');
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 and easy-to-understand tests. Let’s jump into the demo!
Ready, set, code! 🏎
In case you want to skip the explanation and check the code, I’ll give you the link for the repository with the project already bootstrapped and the tests in place.
As the focus of this article is testing and not explaining how to set up React Native, I suggest bootstrapping your project using react-native init
, which creates a pretty simple and clean React Native project.
Start by installing the dependency and creating the fresh 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:
~ cd testReactNativeDetox
~ react-native run-ios
Time to test! 🔧
Before jumping into testing, you need to have the following prerequisites:
- Xcode installed
- Homebrew installed and updated
- Node.js installed (
brew update && brew install node
) -
applesimutils
installed (brew tap wix/brew; brew install applesimutils;
) -
detox-cli
installed (npm install -g detox-cli
)
Start by adding Detox as a dev dependency for the project.
~ yarn add detox -D
Inside the CLI, they provide a command that can automatically set up the project. You need to run:
~ 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";
This will create a new folder called e2e
with a basic test and some initial configuration such as init.js
, which is the file that tells jest
to start the simulator and so on. Let’s modify this initial test to check if the two first sections are visible.
describe('Example', () => {
beforeEach(async () => {
await device.reloadReactNative();
});
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 a configuration for Detox inside your package.json
. Add the following object to the detox
key, replacing the name of testReactNativeDetox
with the name of your application:
{
"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"
}
}
}
}
Once done, try to build the application by running:
~ detox build
In case 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.
Finally, time to 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)
Time to make it fancier! 💅
Let’s put those boring and flat sections inside a colorful carousel! Because who doesn’t love them?
[CAROUSEL IMAGE]
In order to save time, I decided to use an existing carousel component built by the community. For this demo, I used react-swipeable-views-native. I’m sure there must be better alternatives out there, but this one was the perfect match for my needs.
Also, in order to generate nice random colors, I used randomColor.
Install both libraries as dependencies for the project:
~ yarn add react-swipeable-views-native randomcolor
Then I made a few modifications inside App.js
— you can find the code here. This is the summary of changes:
- Wrap all the sections inside
SwipeableViews
to enable swiping behavior - Wrap each section inside a custom
View
calledSlide
that implements properties likepadding
andbackgroundColor
- Add a
Button
and aTextInput
component to the last two slides
And this is the result:
Writing Detox tests 🧪
In order 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.reloadReactNative();
});
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 second test will definitely fail because 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, let’s make Detox move 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!
});
});
At this point, you can execute the end-to-end tests, and they should pass! The command is:
~ 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 slice 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();
});
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!');
});
});
Feature fully tested! 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)
Bonus: Running E2E test in CI 🎁
Running tests inside CI is quite important; they basically eliminate the need to do manual testing and prevent shipping bugs to production (in case we have the proper set of tests). 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.
In case you are 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 Documentation of Detox a little bit, and this is the one that works 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
One last thing: add the command e2e:ci
into your package.json
. This command will build the application (detox build
), run the tests (detox test
), and close the emulator in order to finish the execution (--cleanup
flag).
{
"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 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 will call Travis, and this one runs the Detox tests.
Here is the link to the full log in Travis for that pull request.
Closing words
In case you were thinking of adding tests to your React Native application, I highly encourage you to go and try Detox! Detox is an amazing end-to-end testing solution for mobile, and after using it for quite some time, these are the pros and cons:
- ✅ Very well abstracted syntax for matchers and to trigger specific actions
- ✅ Integration with Jest is just wonderful
- ✅ Possibility 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 the GitHub Issues
Let’s keep building stuff together! 👷
References and further reading
- Demo Repository
- Detox Documentation
- react-swipeable-views-native
- react-native-cli
- randomColor
- End-to-End Testing
- Wix Engineering
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post End-to-end testing in React Native with Detox appeared first on LogRocket Blog.
Top comments (1)
Wix have recently sunsetted their Detox recorder. We're working on an alternative at Moropo - feel free to message if you want pointers.