Writing e2e tests for React Native using Expo

stiv_ml

Stiv Marcano

Posted on May 15, 2020

Writing e2e tests for React Native using Expo

End to End (e2e) testing is a technique that helps ensure the quality of an app on an environment as close as real life as possible, testing the integration of all the pieces that integrate a software. On a mobile app, this could be particularly useful given the diversity of devices and platforms our software is running on top of.

Due to the cross-platform nature of React Native, e2e testing proves to be particularly messy to work on, since we have to write all of our tests with this in mind, changing the way we access to certain properties or query elements no matter the tool we use for connecting to it. Still, tools like Appium and WebdriverIO allow us to work over a common and somewhat standard interface.

The following instructions assume we already have a React Native app built with expo, and use Jest for our unit-testing solution.

Disclaimer: The following instructions are based on a windows machine running an android emulator, output/commands may vary slightly on different architectures.

Setting Up Appium

  • Install required dependencies
$ npm i -D webdriverio babel-plugin-jsx-remove-data-test-id concurently

WebdriverIO will work as our “client” for the appium server in the case of JS. More to come on how to use other clients such as python.

babel-plugin-jsx-remove-data-test-id will help us remove unwanted accessibilityLabels from our app, since that’s the preferred way of targeting elements for both IOS and Android platforms

concurrently will help us automate the running of appium and jest to do our e2e tests

  • Install Appium Doctor
$ npm install appium-doctor -g

This will help us identify if we have all of the needed dependencies to correctly run appium in an emulator.

  • Run Appium Doctor

Depending on the host OS we want to test in, we could run

$ appium-doctor --android

or

$ appium-doctor --ios

For this particular case I’ll be running the android version. This will prompt us with some output on the console, if we have all the required dependencies installed we should see a message similar to the following:

Dependencies requirements

If not all necessary dependencies are met at this point, instead of checkmarks before any given item you’ll see a red X symbol. Check the end of the input for more information on how to fix the particular Issues you’re prompted.

We’re not going to fix the optional requirements that appium-doctor prompts us for the time being, feel free to go over those once you have the testing solution working.

  • Run Appium

By this point, you should be able to run appium without any issues, in order to do so just type

$ appium

You should see something similar to

All dependencies met

If you do so, Congrats! you have correctly set up appium.

Now, lets set up our tests.

Write tests once, run in any platform

One of the key features of react native is its ability to write code once and run it in both iOS and Android, so we want our tests to behave in the same way. There are some limitations for this, since the only way we can write a selector for both platforms is through the accessibilityLabel attribute in react native. This may become an issue if your app depends on accessibility features, make sure to use correct, semantic and descriptive accessibility labels at any place you intend to use them.

If a great accessibility is not on the scope of your current project (it should), you can use accessibilityLabel as a perfect target for querying your elements, just make sure you don’t accidentally worsen the experience of people using screen readers or any other assistive technology. In order to do this we’re going to configure our babel setup to remove the accessibility labels whenever we build for production.

/// babel.config.js

module.exports = function() {
  return {
    presets: ['babel-preset-expo'],
    env: {
      production: {
        plugins: [
          [
            'babel-plugin-jsx-remove-data-test-id',
            { attributes: 'accessibilityLabel' },
          ],
        ],
      },
    },
  };
};

Let’s write our first test now:

I’ve created a called LoginTest.spec.js inside a new folder called e2e, inside the file I have the following:

// myapp/e2e/LoginTest.spec.js

import wdio from 'webdriverio';

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

const opts = {
  path: '/wd/hub/',
  port: 4723,
  capabilities: {
    platformName: 'android',
    deviceName: 'emulator-5554',
    app: 'my-app-name.apk',
    automationName: 'UiAutomator2',
  },
};

describe('Expo test example', function() {
  let client;
  beforeAll(async function() {
    client = await wdio.remote(opts);
    await client.pause(3000);
    const pack = await client.getCurrentPackage();
    const activity = await client.getCurrentActivity();
    await client.closeApp();
    await client.startActivity(pack, activity); //Reload to force update
    await client.pause(3000);
  });

  afterAll(async function() {
    await client.deleteSession();
  });

  it('should allow us to input username', async function() {
    // Arrange
    const field = await client.$('~username');
    const visible = await field.isDisplayed();
    // Act
    await field.addValue('testUsername');
    // Assert
    expect(visible).toBeTruthy();
    expect(await field.getText()).toEqual('testUsername');
  });
});

That may be a lot of new code to digest at once, so let’s go line by line:

import wdio from 'webdriverio';

First, we import the WebdriverIO client. This is the main package that will include the functionality we need to query elements from the app and simulate events on the emulator.

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

This will tell our test runner (in this case jest) to make the tests error after a certain amount of ms has passed. Here we’re setting it explicitly in the test, but if you’re using jest you can modify the testTimeout property on your jest configuration. If you’re using any other test runner I’d recommend going through their documentation, most of them have a similar property.

const opts = {
  path: '/wd/hub/',
  port: 4723,
  capabilities: {
    platformName: 'android',
    deviceName: 'emulator-5554',
    app: 'my-app-name.apk',
    automationName: 'UiAutomator2',
  },
};

These are the configurations for our driver to know what to look for when using the appium interface to query for elements and save elements. You can read more about them here.

You can get the device name going to your emulator > help > about

In order to generate an apk from expo, you have to run the command

expo build:android    

And wait in the queue for it to build.

In this case, I placed the downloaded apk in the root folder for my project, and renamed it my-app-name.apk.

Since we’re using WebdriverIO, the automationName will be UiAutomator2, since that’s how appium recognizes it.

Since lines 18-33 is mostly setup, we won’t focus on that for now, next part focuses on line 34 and forward

Writing the actual test

The idea of this test is just to showcase a normal flow on a test, therefore we will be dealing with a fairly simple use case: Checking that we have a valid username input

const field = await client.$('~username');
const visible = await field.isDisplayed();

The first line allow us to query an item by accesibilityLabel as I previously mentioned, for more information about specific selectors go to the appium documentation and the WebdriverIO documentation.

The second line checks whether our previously selected item is visible on the current screen, more information here.

await field.addValue('testUsername');

This line simulates user typing into the selected field, in this case, we’re inserting the ‘testUsername’ text inside the previously selected username field.

expect(visible).toBeTruthy();
expect(await field.getText()).toEqual('testUsername');

Lastly, we use Jest to check that the field is indeed visible on our Login Screen, and that the text on the given username field is the same as the one we wrote in it.

Running the test

Since we’re using Jest as our test runner on our react app, I’ve set up a command on my package.json to run both the appium server and run Jest in watch mode, it looks like this:

Command for running appium and jest at the same time

Here we’re using concurrently, a simple npm package that allows us to run several npm scripts at the same time, in this case we run the appium server and jest in watch mode, add their names and different colors to easily recognize them in the console, and pass the standard input to the jest command, so we can narrow down our tests or do things like run coverage reports.

With this done, we simply have to run npm run test:e2e on our console, and expect something like this
Example output of running npm run test:e2e

to be run, and something like this

Example output when all tests are successful

to be the output. If so, congratulations, you’ve correctly set up your integration tests for your react native app

Wrapping up

While we’re far away from calling it a day on our e2e testing solution, the main setup It’s done. Next steps include integrating it with a CI/CD pipeline, and making it work on IOS platforms.

Further Reading

Photo by freestocks on Unsplash

💖 💪 🙅 🚩
stiv_ml
Stiv Marcano

Posted on May 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related