July 15, 2020   |   14min read

How to Apply UI Test Automation in React Native Apps?

React Native has changed the way we think about the mobile apps development process but didn’t change the main goal of software development teams—delivering the best quality products as soon as possible. However, it definitely comes with its own set of challenges every developer needs to conquer. I will do my best to outline the most important ones that come with React Native UI testing, along with some tips on how to solve them. I have also prepared a short guide to React Native with Detox (in my opinion the best hybrid testing framework) and how to set it up for Continuous Integration systems.

So, how to test a React Native app? Dive in!

Challenges for React Native UI testing

More complex setup

For React Native debug apps the entire JavaScript code is bundled via a React Native packager. It means that we have to take care of launching and terminating the packager too. Some frameworks often require running their own services in the background—e.g Appium depends on Appium Server, which makes test setup quite complicated.

In one of the frameworks I’ve evaluated, React Native Detox code that is used for UI automation should be placed both in Javascript files and platform-specific files. It leads to the configuration mess when you have to handle npm / yarn dependencies and resolve conflicts in platform-specific libraries.

Check if React Native
is the right tech for your project

View elements recognition

The main feature of React Native is that all views are generated automatically for both platforms using JavaScript code. That’s convenient, but if we decide to write some test scripts using native frameworks, the view hierarchy inspected on iOS and Android can surprisingly differ…

For example, let’s consider a text view from a sample app defined like:

accessible= {true}
testID= {"ButtonText"}
accessibilityLabel= {"ButtonTextDesc"}
{!this.state.clicked? defaultButtonText: "Polidea"}
view raw CustomButton.js hosted with ❤ by GitHub

As on the screenshots below, for iOS testID is interpreted as a React Native accessibility identifier but for Android, it’s missing. If you’ve decided to localize view elements by texts you can use the React Native accessibility label. Hopefully, accessibilityLabel and the text are interpreted almost the same for both platforms.



Platform-specific issues


The first thing you will surely come up against is dealing with app permissions. Make sure that each test can be launched separately and that a permission pop up will not ruin the execution. For native scripts, there are a lot of plugins and ways to sort out this problem. Some hybrid frameworks can also take care of this (warning: Cavy doesn’t!) so don’t be afraid to check them out.

Shared state

Some app states and local databases are sometimes shared between separated tests. This problem really affects everyone who works on React Native UI testing automation. In order to reduce the risk of shared app states between tests, you can use platform specific approaches—e.g Android Test Orchestrator (described in more detail in one of our blog posts) or implement custom methods that clear cache and memory files or remove any kind of data. React Native dedicated frameworks that I’ve checked (Cavy, Detox) also cover that, together with implemented cleanup methods.


Native frameworks

Native approaches give more white-box solutions for common problems. What’s also really helpful, is that the open source community gives more tools and frameworks that make testing easier, e.g. mocking tools, simulator hacks. Native frameworks make deeper integration with the app possible. By adding extra identifiers to view elements, you can make all of them as accessible as the native apps.


Judging by popularity, speed and entry level, I recommend using Espresso (for a single-package testing) and UIAutomator (for a multiple-package testing). As I mentioned before, React Native doesn’t support setting up resource-id attribute for view elements from JavaScript code (only by setting it up directly in R.xml file). It’s a problem as matching by resource-id is a standard practice when using Espresso and using it with React Native means limiting yourself to using only a text and content-description selectors.


There are few ways of doing native iOS automation. You can use XCUITest if you are looking for a low-cost setup solution (provided directly from Xcode), but depending on your needs, you can also use more powerful, mature frameworks that are based on XCUITest.

Google also remembers about iOS testers and released the EarlGrey framework—a tool similar to Espresso but running on iOS. Fast, reliable and powerful tool deeply integrated within the app source code that allows many users actions.

Cross-platform frameworks

There are ways to avoid writing a code for each platform that we develop—just like we do by using React Native.

I did a research on React Native dedicated tools and it turns out there are plenty of frameworks that allow for writing one code for both platforms:

I’ve decided not to write more about Appium because of its very poor performance and complex setup. Another framework I considered was Cavy. All Cavy matchers are based on custom testHook identifiers (cannot refer to text values), which makes Cavy very fast but they need to be put in each view element. What’s more, Cavy affects the index file and it’s a big change when it comes to apps. Finally, I discovered Detox—automation framework for React Native and Native applications. Detox combines EarlGrey and Espresso—native automation stack for iOS and Android. Native stack makes Detox very fast, powerful and easy for CI integration.

I’ve prepared a simple guide on how to use Detox with CI server (Gitlab CI).

Test automation guide with Detox

I’ve created a very primitive app, which transforms a button into the Polidea logo. It was simple, but enough to show you how to test a React Native app.

I’ve created a sample test class where I’ve defined 4 test methods which use Detox API and access views by text and react native test ID. Notice how easily you can check the appearing views.

describe('Example', () => {
beforeEach(async () => {
await device.reloadReactNative();
await waitFor(element('ButtonText'))).toBeVisible().withTimeout(10000);
it('should Press me text be displayed', async () => {
await waitFor(element(by.text("Press Me"))).toBeVisible().withTimeout(100);
it('should ButtonText id be displayed', async () => {
await expect(element('ButtonText'))).toBeVisible();
it('should Polidea logo be displayed after click on button', async () => {
await element('ButtonText')).tap();
await waitFor(element('ButtonImage'))).toBeVisible().withTimeout(2000);
it ('should Press me text be morphed in Polidea title after click on button',async () =>{
await element('ButtonText')).tap();
await waitFor(element(by.text("Press Me"))).toNotExist().withTimeout(2000);
await waitFor(element(by.text("Polidea"))).toBeVisible().withTimeout(100);
view raw firstTest.spec.js hosted with ❤ by GitHub

I’ve created a configuration for running Detox both on Android and iOS as below:

"detox": {
"configurations": {
"ios.sim.debug": {
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/",
"build": "xcodebuild -project ios/PolideaSample.xcodeproj -scheme PolideaSample -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
"type": "ios.simulator",
"name": "iPhone 6s Plus"
"android.emu.debug": {
"binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
"cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=debug && cd ..",
"type": "android.emulator",
"name": "Pixel_API_26"
view raw package.json hosted with ❤ by GitHub

React Native app with tests configured as above can be built by using these commands:

detox build -c ios.sim.debug & detox build -c android.emu.debug

For running those tests on Android and iOS just execute:

detox test -c ios.sim.debug & detox test -c android.emu.debug

As you can see, running Detox tests is very simple and can be easily moved to the CI system. Let’s take a look at a plan for CI integration—all we need is to include the following steps inside our test job:

  • Install missing project dependencies and Detox
  • Launch React Native packager
  • Build an app using Detox
  • Run Detox tests
  • Terminate the packager

You can check it on my simplified (no caching, no separate steps) iOS and Android snippet for Gitlab CI setup:

- build
- test
- deploy
- (if [ "$(lsof -n -i4TCP:2137)" != "" ]; then kill -9 $(lsof -n -i4TCP:2137); else echo "Cleaned"; exit 33; fi);
stage: test
- brew tap wix/brew
- brew install --HEAD applesimutils
- npm install -g detox-cli
- npm install -g react-native-cli
- npm install
- react-native start --port 2137 &
- detox build -c ios.sim.debug
- detox test -c ios.sim.debug
- kill -9 $(lsof -n -i4TCP:2137)
- xcode-9.2
stage: test
- name: android-emulator:latest
alias: pixel
entrypoint: ["/", "android-23", "x86", "pixel"]
- adb connect pixel:5555; sh ./scripts/ pixel
- mkdir -p ./detox_node/
- npm install --prefix ./detox_node/ -g detox-cli
- npm install --prefix ./detox_node/ -g react-native-cli
- npm install
- ./detox_node/bin/react-native start --port 2137 &
- ./detox_node/bin/detox build -c android.emu.debug
- ./detox_node/bin/detox test -c android.emu.debug
- kill -9 $(lsof -n -i4TCP:2137)
- android-emu
view raw .gitlab-ci.yml hosted with ❤ by GitHub

As the result for detox_test:iOS you’ll get the output:



Latest Detox releases come with the third-party drivers support. It allows for even more powerful usage of Detox. One of the opportunities this feature gives is a third-party driver detox-puppeteer. The driver allows for writing test automation with Detox for Web projects. It may be extremely useful for projects which combine mobile and web.

In order to install this framework use npm install detox-puppeteer and add following json part into your Detox configuration:

"web.example": {
"binaryPath": "", // your webiste url
"type": "detox-puppeteer",
"device": {
"defaultViewport": {
"width": 2560,
"height": 768
"headless": false,
"devtools": false
"name": "puppeteer"
view raw json hosted with ❤ by GitHub

You can try this with the test code example:

describe('Example Web', () => {
beforeEach(async () => {
await device.disableSynchronization();
it('should have welcome screen', async () => {
await expect(element(by.text('Development studio delivering digital products.')))
it('should have Get in touch section', async () => {
await element(by.text('Get in touch')).tap();
await expect(element(by.text('Let us know about your project!')))
view raw web.e2e.js hosted with ❤ by GitHub


In my article, I’ve presented approaches for React Native UI test automation and explained how easily it can be achieved with a Detox framework. The solution I’ve provided is dedicated for Gitlab CI that is used by Polidea for Continuous Integration, but you can easily adapt those steps to your needs. You can find my sample project on Polidea github.

If you want to test your React Native app and need help—get in touch!

Adam Stasiak

Senior Test Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!