share

ENGINEERING

11min 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 import ones that come with React Native 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.

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, view hierarchy inspected on iOS and Android can surprisingly differ…

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

<Text
accessible= {true}
testID= {"ButtonText"}
accessibilityLabel= {"ButtonTextDesc"}
style={styles.textStyle}>
{!this.state.clicked? defaultButtonText: "Polidea"}
</Text>
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. Hopefully, accessibilityLabel and the text are interpreted almost the same for both platforms.

image

image

Platform-specific issues

Permissions

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 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.

Options

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.

Android

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.

iOS

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 for this guide’s purpose.

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

describe('Example', () => {
beforeEach(async () => {
await device.reloadReactNative();
await waitFor(element(by.id('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(by.id('ButtonText'))).toBeVisible();
});
it('should Polidea logo be displayed after click on button', async () => {
await element(by.id('ButtonText')).tap();
await waitFor(element(by.id('ButtonImage'))).toBeVisible().withTimeout(2000);
});
it ('should Press me text be morphed in Polidea title after click on button',async () =>{
await element(by.id('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/PolideaSample.app",
"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",
"build":
"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:

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

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

image

Conclusion

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.

share


AdamSenior Test Engineer