Recently some of us have been working on a React Native project to improve our skills and learn more about mobile development. The tech background for the majority of us is Ruby and Ruby on Rails so you can imagine that we love to TDD the features we create.
We want to bring this practice to our React Native project and we found Detox as a great tool to create end-to-end tests. When using Detox with Expo and TypeScript, a lot of different configurations and steps are required. The documentation exists for each tool but is scattered over the internet. We decided to write this guide to help others that are in the same situation as us.
Pre-requisites
In this post we will assume that you have setup an Expo React Native project with TypeScript and Jest for the testing library.
A few tools are required to have a fully working testing environment. While they are not always directly linked with the purpose of this article, we found that some steps were suffering from a lack of documentation on the internet. We are going to list some important requirements for the sake of helping you have everything set up, but please feel free to jump to the next section if you want to start with Detox immediately.
Java
is required to run the tests on Android and applesimutils is needed to
run the tests on iOS.
Java
You can install Java using Homebrew:
brew install java
Make sure Java is properly installed and available in your terminal
using java -version
. You should see a result like:
openjdk version "21.0.2" 2024-01-16
OpenJDK Runtime Environment Homebrew (build 21.0.2)
OpenJDK 64-Bit Server VM Homebrew (build 21.0.2, mixed mode, sharing)
Otherwise, one workaround is to create a symlink to the Java installation:
sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk.jdk
applesimutils
You can install applesimutils
using Homebrew:
brew tap wix/brew
brew install applesimutils
Path to node_modules
binaries
As a final note, you should have access to the bin
directory
from node_modules
in your path.
# .zshrc or .bashrc
export PATH=$PATH:./node_modules/.bin
What is Detox and why should I use it?
Detox is a testing library for React Native that allows us to write end-to-end tests for both iOS and Android applications. Thanks to Detox we can write end to end tests of our features in a way that simulates the user interactions with the application.
In the documentation of Detox we can find the following features that make it a great tool for testing React Native:
- Cross Platform: Write end-to-end tests in JavaScript for React Native apps (Android & iOS).
- Debuggable: Modern async-await API allows breakpoints in asynchronous tests to work as expected.
- Automatically Synchronized: Stops flakiness at the core by monitoring asynchronous operations in your app.
- Made For CI: Execute your E2E tests on CI platforms like Travis CI, Circle CI or Jenkins without grief.
- Runs on Devices: Gain confidence to ship by testing your app on a device/simulator just like a real user (not yet supported on iOS).
- Test Runner Agnostic: Detox provides a set of APIs to use with any test runner without it. It comes with Jest integration out of the box.
Let’s start! Adding Detox to our project
First we need to install Detox in our project as a development dependency. Yarn is our default utility but the commands in this article can be performed with other tools like NPM, NPX and others.
yarn add --dev detox
yarn add --dev @config-plugins/detox
We then need to add or update the "plugins"
key in app.json
, to include the
detox plugin to add support for Android.
{
"expo": {
"plugins": ["@config-plugins/detox"]
}
}
Prebuild
Expo provides a prebuild
command to generate native code for a React Native
project. This step is necessary for us as it will create the package/bundle
structure and configuration that Detox depends on.
yarn expo prebuild
We will be prompted with a few questions related to the name of the package.
Configure Detox with Jest
If you don’t have Jest declared as a development dependency, you can add it with the following method.
yarn add --dev jest
Detox provides an init
command that supports different testing environments,
which is Jest in our case. It generates a new e2e
folder where the Jest
configuration and tests resides and .detoxrc.js
file at your project root to
configure Detox.
yarn detox init -r jest
By default, the command will create a starter test that you can keep, change or delete. We will add our own test later in this article.
.detoxrc.js
configures the simulators/emulators used during the tests,
and the name of the packages. Feel free to check the devices
key and ensure
Detox will use the devices you intend to.
You will find the value YOUR_APP
multiple times in the file. This value needs
to be updated with the name of your project. We named ours Pokedex
. You might
want to check the name created in the ios/
folder, generated by
theexpo prebuild
command, if you are ensure about the name of the format for
your application.
module.exports = {
// ...
devices: {
simulator: {
type: "ios.simulator",
device: {
type: "iPhone 15",
},
},
emulator: {
type: "android.emulator",
device: {
avdName: "pixel_4",
},
},
},
};
Configuring Jest with TypeScript
While this step is not necessary, we recommend using TypeScript in your
project and in your tests. This will add consistency, confidence and
efficiency to your testing process. Let’s first add the dependencies.
yarn add --dev ts-jest @types/jest
We also need to change the supported test file extensions
in e2e/jest.config.js
, to support JavaScript and TypeScript files:
module.exports = {
// ....
testMatch: ["<rootDir>/e2e/**/*.test.[jt]s?(x)"],
};
Adding our first test
At this point, all the configuration should be over, but we still need to create
a brand new test and generate the
packages for Detox to run the tests. Let’s add a new test in the e2e/
directory:
// e2e/App.test.ts
import { expect, by, device, element } from "detox";
describe("Home screen", () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it("shows a visible button", async () => {
await expect(element(by.text("Open Pokedex"))).toBeVisible();
});
it("shows the surprise after tapping the button", async () => {
await element(by.text("Open Pokedex")).tap();
await expect(element(by.text("Gotta Catch ’Em All"))).toBeVisible();
});
});
Building our debug iOS and Android apps
We are getting close to running our first end-to-end test! But first we need to build the debug versions of our iOS and Android apps. The Apple platform needs an extra step to install all the dependencies.
cd ios
pod install
cd ...
We can now build the apps:
detox build --configuration ios.sim.debug
detox build --configuration android.emu.debug
Running the test
Finally we can run the test! First, make sure the iPhone Simulator and the Android Emulator are up and running.
Then, each of the following commands will run our end-to-end test on the appropriate platform.
detox test --configuration ios.sim.debug
detox test --configuration android.emu.debug
Now is the time to celebrate our first big victory: A failing test suite! 🤡
The failing test should look like this:
This is actually a very important goal we just reached: Our testing environment is now configured and working and we are already engaged in our TDD process. Now let’s make this test pass.
Let’s add our business code
Here is one of the simplest implementations we can produce to satisfy the test’s requirements.
// App.tsx
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
export default function App() {
const [clicked, setClicked] = useState(false);
return (
<View style={styles.container}>
{!clicked && (
<Pressable style={styles.button} onPress={() => setClicked(true)}>
<Text style={styles.text}>Open Pokedex</Text>
</Pressable>
)}
{clicked && <Text style={styles.surprise}>Gotta Catch ’Em All</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
text: {
fontSize: 16,
fontWeight: "bold",
color: "white",
},
surprise: {
fontSize: 30,
color: "#ff0000",
},
button: {
paddingVertical: 12,
paddingHorizontal: 32,
backgroundColor: "#ff0000",
},
});
Now, if all the configuration went well, running the test commands again should show you a passing test suite.
Congratulations! 🎉
Adding new scripts to our package.json
To simplify the introduction of end-to-end tests in your testing process, we
suggest to add a simple custom command in package.json
which runs Detox on both
platforms at the same time.
{
"test:detox:android": "detox test --configuration android.emu.debug",
"test:detox:ios": "detox test --configuration ios.sim.debug",
"test:detox": "yarn test:detox:ios && yarn test:detox:android"
}
Now we can run the tests with a single command:
yarn test:detox
Conclusion
We have seen how to setup Detox in a React Native project with TypeScript and Jest. We have also seen how to configure Jest with TypeScript and how to build the debug versions of our app for iOS and Android. We have also written our test first, and then implemented the code to make the test pass following the TDD approach.
We hope this guide helps you to setup Detox in your project. We now have a great tool configured to continue writing end-to-end tests and make sure that any feature we add to our application is working as expected.