Functional testing your react-native app

react-native is a great framework to build cross-platform apps.

It is build by facebook, and had it’s first release(iOS only) back in March 2015. Since then it has grown a great community and with joined efforts it has been optimized to run really well on both iOS and Android.

One challenge when building cross-platform applications is to settle on a tool-set that enables developers to produce value that benefits both platforms, without needing to be either an android or iOS expert.

I’ve taken up that challenge in regards to automate functional-tests, that are platform agnostic and doesn’t require too much knowledge of either platform. I also want to write the tests in the same language as the application(javascript).

In order to achieve this, I have found the tool webdriver.io which is a Webdriver written in modern javascript. Webdriver is a protocol originally made for creating functional tests for the browser, but since then has had it’s entry into the mobile testing world with for example appium.

Stack of tools:

  • appium server 1.7.1
  • webdriverio v4.8.0

First start by installing appium-server using npm. You could choose to install this as a devDependency in your react-native project, but in my experience it’s more of a hassle because it’s quite big, and contains large binary files.

npm install -g appium@1.7.1

Installation of webdriverio comes later — we’ll add it as a dev-dependency on the project.

After hundreds of log-output and lots of dependencies resolved, you have appium available from you command-line. Verify by running the command

➜  ~ appium
[Appium] Welcome to Appium v1.7.1 (REV e1c84bae37afae282f39b91025435c3717e6d0ab)
[Appium] Appium REST http interface listener started on 0.0.0.0:4723

Ok. Now control-c to exit appium, and we’re ready to configure the actual react-native application.

Currently I do not have any open source react-native application, and I do not want to write one just for this article — lazy me :)

For this blog post I have chosen a very simple npm dashboard application called ndash. My reasoning for this is that it is very simple, and doesn’t have too much integration complexity as for example authentication.

Clone this repo from Github:

git clone https://github.com/alexindigo/ndash.git

Identifying elements

webdriver.io has multiple ways of selecting elements for interaction. Some are specific for web, some cross-platform, and some are specific to mobile.

You can use android specific selectors by using the UIAutomator API and use ios specific selectors using Apple’s UI Automation framework. However as stated previous in the article, I do not like the fact that the developer needs specific knowledge about a certain platform and there is actually one more opportunity, that does not require platform-specific knowledge and comes with a very good side-effect — Accessibility selectors !!

It uses accessibility abilities on each platform to identify the item we’re looking for. A side-effect of this is, as you might have guessed, that your application will have great support for users who need to use the build-in accessibility tools on the platform they’re on.

Preparing for accessibility

Let’s look into the app.

In this test, I want to test adding a profile. This means that my script will have to:

  1. Identify and press the menu
  2. Identify and press the add button
  3. Input the name of the profile
  4. Press OK to submit

Ok, the code changes.

First I will add accessibility properties to the menu. The author of ndash has called this HomeButton. Now, for accessibility properties I like to make sure that I add these to the instance of the component, and not just hardcode them onto the component it self.

I do this by going into app/component/HomeButton.js and extracts properties onto the TouchableOpacity root component. Then adds accessible=true and accessibilityLabel={'Home button'} on the component instance in app/components/Navbar.js.

diff --git a/app/components/HomeButton.js b/app/components/HomeButton.js
index dda6a77..663eb60 100644
--- a/app/components/HomeButton.js
+++ b/app/components/HomeButton.js
@@ -15,6 +15,8 @@ export default class HomeButton extends Component {
return (
<TouchableOpacity
onPress={this.props.onAction}
+ accessible={this.props.accessible}
+ accessibilityLabel={this.props.accessibilityLabel}
>
<View
style={stylesContainer}
diff --git a/app/components/Navbar.js b/app/components/Navbar.js
index a98fe10..2f871af 100644
--- a/app/components/Navbar.js
+++ b/app/components/Navbar.js
@@ -28,6 +28,8 @@ export default class Navbar extends Component {
};
const homeButton = <HomeButton
+ accessible={true}
+ accessibilityLabel={'Home button'}
onAction={this.props.onHomeButton}
style={styles.homeButton}
/>;

Now onto the MenuButton. Same story. I go into app/component/MenuButton.js and extracts properties onto the TouchableOpacity root component. Then adds accessible=true and accessibilityLabel={'Home button'} on the component instance in app/scenes/Menu.js.

diff --git a/app/components/MenuButton.js b/app/components/MenuButton.js
index 072d373..cd5de3a 100644
--- a/app/components/MenuButton.js
+++ b/app/components/MenuButton.js
@@ -35,7 +35,11 @@ export default class MenuButton extends Component {
}
return (
- <TouchableOpacity onPress={this.props.action}>
+ <TouchableOpacity
+ onPress={this.props.action}
+ accessible={this.props.accessible}
+ accessibilityLabel={this.props.accessibilityLabel}
+ >
<Image
style={this.props.style}
source={image}
diff --git a/app/scenes/Menu.js b/app/scenes/Menu.js
index 95ac93a..f8b8dcc 100644
--- a/app/scenes/Menu.js
+++ b/app/scenes/Menu.js
@@ -141,6 +141,8 @@ export default class Menu extends Component {
: <MenuButton
style={[styles.menuButton, styles.menuButtonRight]}
image="add_profile"
+ accessible={true}
+ accessibilityLabel={'Add profile button'}
action={this.addProfile.bind(this)}
/>
}

And last: in order to assert that the profile is actually added to the list of profiles, we will add accessibility properties to the list of profiles in the menu. This time the properties is already extracted onto the MenuItem component.

diff --git a/app/scenes/Menu.js b/app/scenes/Menu.js
index f8b8dcc..799bcbf 100644
--- a/app/scenes/Menu.js
+++ b/app/scenes/Menu.js
@@ -204,7 +204,7 @@ class ProfileItem extends Component {
}
return (
- <MenuItem action={this.props.action}>
+ <MenuItem action={this.props.action} accessible={true} accessibilityLabel={`Profile: ${this.props.profile.handle}`}>
<View style={styles.menuContainer}>
<Userpic
style={styles.menuProfileImage}
@@ -242,7 +242,7 @@ class ProfileItem extends Component {
class MenuItem extends Component {
render () {
return (
- <TouchableOpacity style={{flex: 1}} onPress={this.props.action}>
+ <TouchableOpacity style={{flex: 1}} onPress={this.props.action} accessible={this.props.accessible} accessibilityLabel={this.props.accessibilityLabel}>
{this.props.children}
</TouchableOpacity>
);

Writing and running tests

The app is actually ready to be tested now, so let’s install webdriverio as a dev-dependency on the project.

npm install --save-dev webdriverio

Setup webdrivers runner called wdio by calling wdio binary from webdriverio

➜  ndash git:(master) ✗ ./node_modules/.bin/wdio
=========================
WDIO Configuration Helper
=========================
? Where do you want to execute your tests? On my local machine
? Which framework do you want to use? jasmine
? Shall I install the framework adapter for you? Yes
? Where are your test specs located? ./__functional__/*.spec.js
? Which reporter do you want to use? spec - https://github.com/webdriverio/wdio-spec-reporter
? Shall I install the reporter library for you? Yes
? Do you want to add a service to your test setup? appium - https://github.com/rhysd/wdio-appium-service
? Shall I install the services for you? Yes
? Level of logging verbosity verbose
? In which directory should screenshots gets saved if a command fails? ./errorShots/
? What is the base url? http://localhost
Installing wdio packages:
Packages installed successfully, creating configuration file...
Configuration file was created successfully!
To run your tests, execute:
$ wdio wdio.conf.js

Almost there! The above Configuration Helper generated a file called wdio.conf.js in the root of your project. We need to make some adjustments to that in order to add appium start args and align capabilities to run our app.

diff --git a/wdio.conf.js b/wdio.conf.js
index b0ad844..e39dc90 100644
--- a/wdio.conf.js
+++ b/wdio.conf.js
@@ -42,9 +42,9 @@ exports.config = {
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
// grid with only 5 firefox instances available you can make sure that not more than
// 5 instances get started at a time.
- maxInstances: 5,
- //
- browserName: 'firefox'
+ platformName: 'iOS',
+ deviceName: 'iPhone 6',
+ app: `${__dirname}/ios/build/Build/Products/Debug-iphonesimulator/ndash.app`,
}],
//
// ===================
@@ -107,6 +107,15 @@ exports.config = {
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
services: ['appium'],
+ appium: {
+ args: {
+ address: '127.0.0.1',
+ port: 4444,
+ defaultCapabilities: JSON.stringify({
+ automationName: 'XCUITest',
+ }),
+ },
+ },
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber

That’s it! wdio is setup to run automatically, now all is left is to write the test — AKA the easy part :)

Create the file __functional__/profile_management.spec.js and input the following:

describe('Profile management', () => {
  it('can add a profile, by name', async () => {
    await browser.waitForExist('~Home button');
    await browser.click('~Home button');
    await browser.waitForExist('~Add profile button');
    await browser.click('~Add profile button');
    await browser.keys('jdalton');
    await browser.click('~ok');
    await browser.waitForExist('~Profile: jdalton');
  });
});

Add the run-script wdio wdio.conf.js to the package.json to make it easier to run

diff --git a/package.json b/package.json
index e9ef2c4..7c0acd6 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
- "android": "$(which emulator) -avd Nexus_5_API_23"
+ "android": "$(which emulator) -avd Nexus_5_API_23",
+ "functional": "wdio wdio.conf.js"
},
"dependencies": {
"art": "^0.10.1",

Let’s run it!

First you need to run react-native run-ios just to be sure that the app has been build.

npm run functional

Don’t be fooled by all the initiation-time. Those can be optimized by adjusting the appium start-up args.