Article originally published on the LogRocket Blog.
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 applications 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 take a look at the definition of this testing technique:
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. — Software Testing Dictionary
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 the 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 validate 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
and react-native-ui-lib
.
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 a 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?
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](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
One more thing before you leave, I decided to start a newsletter so in case you want to hear about what I’m posting please consider following it! No SPAM, no hiring, no application marketing, just tech posts 👌
Top comments (0)