Detox for your React Native App
E2E UI Testing explained— Recap of the MD Tech Meetup at UniNow
Introduction
When it comes to building apps, software testing is critical. It assures the quality of the product and satisfies the customers as well as the users. In the web development world, end-to-end testing is a widely adopted technique and because of tools like Cypress, Puppeteer and Playwright it is also easy to do. But when it comes to mobile applications, end-to-end testing is not so common. My guess is that many app developers are discouraged by the setup and configuration needed to add end-to-end testing to their apps.
Detox is a gray box end-to-end testing and automation framework for react native apps. It benefits from JavaScript test files, similar to those Jest uses, that run on both, iOS and Android. And due to its gray boxiness, Detox monitors asynchronous operations in your app and synchronizes them to prevent flakiness. We will show how to set up detox in a react native application and write some basic tests.
For the sake of time, we are limiting ourselves to iOS. Let us know if you want a dedicated walkthrough for Android as well.
Set up detox for your react native application
We assume that you already completed the setup for:
- macOS Catalina or newer
- Xcode (≥ v12.x)
- Xcode command-line tools (
xcode-select -install)
- Homebrew
- Node (≥ v12.0)
- react-native application you can start with
At first, you have to prepare the environment of your react native app. Start by installing the detox command line tools. This package makes it easier to operate detox from the command line and enables usage outside your npm scripts.
npm install detox-cli --global
# or
yarn global add detox-cli
Detox uses a collection of utilities to query and communicate with the simulator, to install them and run the following commands.
brew tap wix/brew
brew install applesimutils
Set up a test runner
Detox delegates the actual JavaScript test-code execution to a dedicated test-runner. It supports the popular Jest and Mocha out of the box. We choose Jest for now. To add Jest to your project run
npm install -D jest@>=27.2.5
Detox provides a configuration setup script that creates all the necessary files for Jest and Detox itself.
detox init -r jest
This should create the following files:
- an
e2e/
folder in your root - an
e2e/config.json
file - an
e2e/environment.js
file - an
e2e/firstTest.e2e.js
file
Make sure the e2e/config.js
file looks like this
{
"maxWorkers": 1,
"testEnvironment": "./environment",
"testRunner": "jest-circus/runner",
"testTimeout": 120000,
"testRegex": "\\.e2e\\.js$",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}
And the e2e/environment.js
file looks like this
const {
DetoxCircusEnvironment,
SpecReporter,
WorkerAssignReporter,
} = require('detox/runners/jest-circus');
class CustomDetoxEnvironment extends DetoxCircusEnvironment {
constructor(config, context) {
super(config, context);
this.initTimeout = 300000;
// This takes care of generating status logs on a per-spec
// basis. By default, Jest only reports at file-level.
// This is strictly optional.
this.registerListeners({
SpecReporter,
WorkerAssignReporter,
});
}
}
module.exports = CustomDetoxEnvironment;
For more information on how to configure Jest with Detox visit the official documentation. (https://wix.github.io/Detox/docs/guide/jest/)
Configure Detox
Now that our environment is ready, there is only one thing left to do. We have to configure detox itself. The general configuration takes place in a /.detoxrc.json
file that is located in your root directory. You have to specify the following fields:
testRunner
specifies which test runner you are usingrunnerConfig
specifies the path to yourconfig.json
apps
can hold multiple app configsdevices
can hold multiple device configsconfigurations
maps apps to devices
You can use the example configuration below to get started.
{
"testRunner": "jest",
"runnerConfig": "e2e/config.json",
"apps": {
"ios": {
"name": "YourAppName",
"type": "ios.app",
"binaryPath":
"ios/build/Products/Debug-iphonesimulator/YourAppName.app",
"build":
"xcodebuild -workspace ios/YourAppName.xcworkspace -scheme YourAppName -sdk iphonesimulator -derivedDataPath ios/build"
}
},
"devices": {
"simulator": {
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
}
}
},
"configurations": {
"ios": {
"device": "simulator",
"app": "ios"
}
}
}
You can get further guidance and clarity from the official docs.
Now everything is set up and you can start by building your app using the detox-cli. Detox executes the build command from your config, so make sure to provide the correct one.
detox build --configuration <your configuration name>
If you are building a debug version of your app, make sure to start the metro bundler beforehand
Write some tests
While detox is offering many ways to find a specific component on your screen (take a look at all the matchers), the best and clearest way is through testID
's. Almost every react-native component can be equipped with a testID
prop, to give it a unique id in your application. (Make sure that the testID you specify is really unique)
<TouchableOpacity
onPress={handleNextButton}
testID={`${testID}.next.button`}
>
<Text>Next</Text>
</TouchableOpacity>
Once your application or screen is decorated with testID
's, you can start with writing the real test.
A detox test looks very similar to a general JavaScript test. At the top level, there is a describe
block, that groups tests of the same flow/screen together. You can define lifecycle hooks inside the describe
block to execute certain actions, like reloading the application to start from a fresh app state. And then there are the it
blocks that contain a composition of actions, matchers, and expectations. The test structure could look like this:
- Validate the starting point
- Perform action
- Expect a certain outcome
And as code:
describe("Onboarding Screen", () => {
beforeAll(async () => {
await device.launchApp({
newInstance: false,
permissions: { location: "always", notifications: "YES" },
});
});
beforeEach(async () => {
await device.reloadReactNative();
});
it("should swipe through onboarding pages", async () => {
// Validate starting position
await expect(element(by.id("onboarding.screen"))).toBeVisible();
await expect(element(by.id("list"))).toBeVisible();
await expect(element(by.id("list.0.item"))).toBeVisible(); // Perform action
await element(by.id("list")).swipe("left"); // Expect certain outcome
await expect(element(by.id("list.0.item"))).not.toBeVisible();
await expect(element(by.id("list.1.item"))).toBeVisible();
});
});
Now it's time to finally run the test and watch detox controlling your device.
detox test --configuration <your configuration name>
Conclusion
Congratulations! 🎊
You successfully prepared your environment, installed and configured detox, and wrote your first test for your react-native application. The first stone on the way to a fully tested app is now set.
After writing this article we have to admit, setting up an end-to-end testing framework is definitely not trivial. And further improvement is needed in this area. Nevertheless, we hope that this article is able to simplify and speed up setting up end-to-end testing for your react-native applications.
Thanks for reading!
We hope you learned something new about detox and end-to-end testing in react native applications. If there are any questions, comment down below, we would love to answer them.
If you want to hear us live — take a look at the upcoming events.
https://uninow.io/