React Native development tools - Part 3: Testing tools

Introduction

This is the final part of a three-part series on React Native development tools where we cover the following tools:

In this part, we’ll cover the following testing tools:

  • Jest - for snapshot testing.
  • Detox - for end-to-end testing.

The goal of this tutorial is to get you up and running with these tools quickly. So expect a lot of setups. Aside from that, you’re also going to see these tools in action. We’ll be using a pre-created React Native app to implement the tools.

What we’re not going to tackle is how you’re going to design your code so it is easy to test. We’re also not going to tackle every feature the tools have to offer. At the end of the tutorial, I’ll be pointing out to a few resources so you can learn more about them.

Prerequisites

To follow this tutorial, you need to have knowledge of developing React Native apps. Optionally, you should also be comfortable with editing config files for Android, as we will be doing a lot of that in this tutorial. We don’t have to do a lot of that in iOS since Detox is more matured on iOS.

To follow this tutorial, you’ll need a computer running Linux or MacOS. I’ve personally tested this tutorial on the following operating systems:

  • Ubuntu 16.04
  • MacOS High Sierra 10.13.5

For Windows, I can’t ensure it will work. But the official docs did mention that the tools should also work on Windows.

Lastly, this tutorial assumes that you have already setup your machine for React Native development.

Setting up the project

Now that that’s out of the way, let’s go ahead and create a new React Native project for the purpose of using the testing tools:

1git clone https://github.com/anchetaWern/RNTesting.git
2    git checkout starter

Take note of the React Native version we have on the package.json file. It’s at version 0.50 even though at the time of writing this tutorial, the latest version is already at 0.56. We’re doing this because Detox is only proven to work for these versions of React Native:

  • iOS: <=0.55
  • Android: <=0.51

In order to target both platforms, we have to downgrade to the lowest of the two. In this case, it should be 0.51. While I was testing it though, I had problems getting Detox to work for React Native version 0.51. That’s why we’re sticking with version 0.50 instead.

Ideally, we want to use the latest version of React Native. But with the current state of Detox, it’s simply not possible. That doesn’t mean that you also have to downgrade the version of your existing project though. You can maintain another copy for testing purposes only. And then gradually upgrade it as support for later React Native versions becomes available.

All the changes introduced in this tutorial are available on the testing branch. You can check that out anytime you feel confused about which specific change you need to implement.

Upgrade to Gradle 3

The next step is to upgrade the Gradle version used by React Native. By default, it still uses version 2, and so do most of the native modules that you’ll find on GitHub (except for projects like React Native Maps). If you’re worried that you’ll no longer be able to use the native modules that still use Gradle 2, then fret not because there’s a way to update those.

So why do we need to upgrade to Gradle 3? That’s because Detox also uses Gradle 3. There’s a way to stick with Gradle 2 but new features that will come to Detox won’t be implemented in there because it uses Detox version 6.

Let’s go ahead and start updating the config files so we can upgrade to Gradle 3. First, update android/build.gradle with the following:

1buildscript {
2      repositories {
3        jcenter()
4        google() // add this
5      }
6      dependencies {
7        // classpath 'com.android.tools.build:gradle:2.2.3' 
8        classpath 'com.android.tools.build:gradle:3.1.0' // replace the above with this
9      }
10      // the rest of the existing config here...
11    }
12    
13    allprojects {
14      repositories {
15        mavenLocal()
16        jcenter()
17        google() // add this
18        maven { ... }
19        // the rest of the existing config here...
20    }

Since this is a new project, your config file will pretty much look the same as mine. If you’re updating an existing project, it’s going to be a bit different, but the overall structure should still be the same.

Next, update android/gradle/wrapper/gradle-wrapper.properties with the new distributionUrl:

    distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

Don’t be confused by the URL when it says gradle-4.4 while we want to use version 3.1. Because we need to set this to a higher version than what we want to use.

Once that’s done, execute react-native run-android to install the new Gradle and verify that the app is working correctly. Don’t forget to launch an emulator instance (either the default Android emulator or Genymotion), or connect an Android device before doing so. Remember that we’re testing though, so practicality-wise, the devices we’ll be running the tests on are just virtual (Android emulator, iOS simulator).

That’s all there is to it when it comes to setting up the project. As you have seen, it’s only the Android config which took up most of the time. For iOS, you don’t really need to perform any additional steps in order to prepare the project for Detox and Jest. As long as you have the latest version of Xcode installed, you should be fine.

Setting up Jest

Jest is the tool that we’ll be using to implement snapshot testing. The main idea behind snapshot testing is that it allows you to test whether a component has changed. The workflow goes something like this:

  • Implement the components.
  • Write tests that will verify if your component is rendering correctly.
  • Run the tests. This will create a yourTestName.snap file for each of your test files inside the _tests_/_snapshots_ directory. These files contain what’s actually being rendered by the components you are testing. We’ll go through this in more detail on a later section.
  • Commit the changes to your version control system.
  • Every time you make a change, you run the tests again. The tests which already have .snap files will have their output compared to that existing snapshot file. If they don’t match, then there might be an unexpected change with your code. Once you’ve verified that the change is correct, you can either update the existing snapshot or revert the changes you made to your component. This way, the tests will pass again.

As you can see, snapshot testing isn’t test-driven development. Mainly because you have to write the component first before taking a snapshot. Snapshot testing is mainly used for checking for unexpected changes in your code, and not for directing how you design your code.

Configuring Jest

When you create a new React Native project, it already comes with Jest pre-installed and configured. Though you’ll still have to perform a few steps to ensure it will work nicely with the other tool that we’ll use (Detox).

On your package.json file, you can see the default configuration options for Jest:

1"jest": {
2      "preset": "react-native",
3    },

Update it to look like the following:

1"jest": {
2      "preset": "react-native",
3      "testMatch": [
4        "<rootDir>/__tests__/*"
5      ]
6    },

This tells Jest to only look for test files inside the _tests_ directory. Because by default, it’s going to run every file that looks like a test file. This means, that when we configure Detox later, it will run Detox’s test files as well. We don’t really want that to happen because they are testing two different things.

Setting up Detox

This section will pretty much go over the same things as the official guides:

In this tutorial though, I’m going to cover some details that aren’t covered in those guides.

With that out of the way, let’s begin.

If you only want to test on Android, you’ll only need to globally install the following:

  • Node 8.3.0 - higher version of Node may also work, but I haven’t personally tested it.
  • Detox CLI

I assume that you have already installed Node by some means. I personally use nvm to manage Node versions. So I installed the required version using the following commands:

1nvm install 8.3.0
2    nvm alias default 8.3.0
3    nvm use default

Once that’s done, verify if version 8.3.0 is being used:

    node --version

After that, you’ll need to install the react-native-cli if you’re using some other Node version before you installed 8.3.0:

    npm install -g react-native-cli

Also install Yarn, we’ll be using it to install the local dependencies for the React Native project:

    npm install -g yarn

Lastly, install the Detox command line tool:

    npm install -g detox-cli

iOS setup

In order to test in iOS, you still need to have a Mac. I assume you already have brew installed so you can simply execute the following commands to install the Apple simulator utilities. Detox uses it to communicate with the iOS simulator:

1brew tap wix/brew
2    brew install applesimutils

Setting up Detox in the project

Now we’re ready to install Detox in the project:

1yarn add detox@8.0.0 --dev
2    yarn add mocha@4.0.1 --dev

We’re installing specific versions because Detox is a relatively new technology. I can’t ensure that this guide will work for you if you installed another version. If you’re just getting started, I recommend you to stick with the versions that we have here to avoid unnecessary headaches. For production cases, you’ll want to use the latest stable version.

Note that Mocha is the test-runner that we’ll be using. But you can also use Jest if you want. Yes, it’s the same thing that we’ll be using for snapshot testing because Jest is a testing framework, it includes everything you need for testing.

Android setup

Follow this section if you want to test for Android, otherwise, proceed to the next one.

The first step is to update android/settings.gradle file to include Detox:

1rootProject.name = 'rntdd'
2    include ':app'
3    
4    # add the below config
5    include ':detox'
6    project(':detox').projectDir = new File(rootProject.projectDir, '../node_modules/detox/android/detox')

Next, under android, update the compileSdkVersion, buildToolsVersion to version 27. Then under android → defaultConfig, set minSdkVersion to 18 and targetSdkVersion to 26:

1// file: android/app/build.gradle
2    
3    android {
4        
5      compileSdkVersion 27 // make sure this is 27
6      buildToolsVersion '27.0.2' // make sure this is 27.0.2
7    
8      defaultConfig {
9        applicationId "com.rntesting" // make sure this is the same as the project name you used when you generated the project with react-native init
10        minSdkVersion 18 // make sure this is 18
11        targetSdkVersion 26 // make sure this is 26
12        
13        // next: add testBuildType, missingDimensionStrategy, and testInstrumentationRunner
14          
15        // rest of the config here...
16      }
17    }

Next, add the following under android → defaultConfig:

1testBuildType System.getProperty('testBuildType', 'debug')  
2    missingDimensionStrategy "minReactNative", "minReactNative46"
3    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

Here’s a breakdown of what we’ve just added:

  • testBuildType - the build type to be used for testing. This can either be debug or release. In this case, we’ll be using debug since we’re still developing the app.
  • minReactNative46 - the minimum React Native version supported by Detox. In this case, the project uses version 0.50 so we need to use minReactNative46 to specify Detox support for React Native version’s that are greater than or equal to version 0.46.
  • testInstrumentationRunner - the native module used for running the tests.

Next, update the dependencies section and add the following. These are Gradle 3 specific configuration, so if your project uses Gradle 2 then you’ll get an error if you add these. These are used to compile the native Java classes that are needed to implement the tests:

1dependencies {
2      compile fileTree(dir: "libs", include: ["*.jar"])
3      compile "com.android.support:appcompat-v7:23.0.1"
4    
5      // add only the following. The above one's already exists
6      androidTestImplementation(project(path: ":detox"))
7      androidTestImplementation 'junit:junit:4.12'
8      androidTestImplementation 'com.android.support.test:runner:1.0.1'
9      androidTestImplementation 'com.android.support.test:rules:1.0.1'
10      
11      compile "com.facebook.react:react-native:+" // existing
12    }

Next, update android/build.gradle to include the following under allprojects → repositories (right after the maven config):

1buildscript {
2      repositories { 
3        google()
4      }
5    }

Lastly, create an android/app/src/androidTest/java/com/rntesting/DetoxTest.java file and add the following. This is the test class that Detox will be using. Be sure to replace “rntesting” in both the file path and the package name below with the actual name of your project if you’re using another one:

1// file: android/app/src/androidTest/java/com/rntesting/DetoxTest.java (note: replace rntesting in this file path with the name of your project if it's different)
2    package com.rntesting; // replace with the actual package name
3    
4    import android.support.test.filters.LargeTest;
5    import android.support.test.rule.ActivityTestRule;
6    import android.support.test.runner.AndroidJUnit4;
7    
8    import com.wix.detox.Detox;
9    
10    import org.junit.Rule;
11    import org.junit.Test;
12    import org.junit.runner.RunWith;
13    
14    @RunWith(AndroidJUnit4.class)
15    @LargeTest
16    public class DetoxTest {
17    
18      @Rule
19      public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
20    
21      @Test
22      public void runDetoxTests() throws InterruptedException {
23          Detox.runTests(mActivityRule);
24      }
25    }

Adding the Detox config

Once installed, you can now update the package.json file to include the detox config:

1{  
2      "name": "RNTesting",
3      // the rest of package.json here...
4       "detox":{  
5          "configurations":{  
6             "ios.sim.debug":{  
7                "binaryPath":"ios/build/Build/Products/Debug-iphonesimulator/rntdd.app",
8                "build":"xcodebuild -project ios/rntdd.xcodeproj -scheme rntdd -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
9                "type":"ios.simulator",
10                "name":"iPhone 7"
11             },
12             "android.emu.debug":{  
13                "binaryPath":"./android/app/build/outputs/apk/debug/app-debug.apk",
14                "build":"cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
15                "type":"android.emulator",
16                "name":"Nexus_5X_API_25_x86"
17             }
18          },
19          "test-runner":"mocha",
20          "specs":"e2e",
21          "runner-config":"e2e/mocha.opts"
22       }
23    }

Breaking down the above config, every setting that has to do with Detox should be inside the detox object. Under the configurations we have the settings to be used by Detox to run the app on the iOS simulator and Android emulator.

Both platforms (Android and iOS) have the following options:

  • binaryPath - the path to where the compiled React Native app is located. These paths are standard to both platforms so you don’t really have to change anything.
  • build - the command for building the app to be used for testing.
  • type - the device type. On iOS, the standard is ios.simulator. While on Android, it depends on what you use for testing. If it’s the Android emulator installed using AVD, the value should be android.emulator. But if you’re using Genymotion, the type should be android.attached. If this is the case, the value of the name should be the IP address and port number of the virtual device. The most common value for this is 192.168.57.101:5555, but you can use the adb devices command to find out. Don’t forget to boot up the virtual device first before doing so.
  • name - the name of the device. On iOS, you can find out the device names you can use by executing xcrun simctl list. While on Android, you can execute avdmanager list avd to get a list of Android emulators installed on your system. If that doesn’t work, you need to find out where Android SDK is installed. After that, go to the sdk/tools/bin directory and execute ./avdmanager list avd. Here’s what the output for those two commands looks like:
rn-dev-tools-testing-list-devices-ios
rn-dev-tools-testing-list-devices-android

Initialize testing code

Once you’ve added the config, the next step is to initialize the project for testing. For Mocha, you use the following command:

    detox init -r mocha

This command creates an e2e directory at the root of the project. The directory contains the following files:

  • init.js - contains the initialization code that runs before any of your tests are executed.
  • mocha.opts - contains the options for running the tests.
  • firstTest.spec.js - the default Detox test file. Open it so you have an idea what a test file looks like. Because that’s what we’re going to work with later.

Compiling the app

The next step is to build the app so that the binaryPath that we added in the Detox config earlier will actually have a runnable app:

1detox build -c ios.sim.debug
2    detox build -c android.emu.debug

The command above compiles the debug version of the app for the configuration (-c or --configuration) that you specified.

That should work, but in case it doesn’t, try running the project by normal means to see if there are any issues that arise:

1react-native run-android
2    react-native run-ios --simulator="iPhone 7"

If you get an issue which looks like this on Android:

rn-dev-tools-testing-annotations-conflict

That’s because the main app uses a different version of the support-annotations library from the one used by the test app. You can solve it by updating the android/app/build.gradle file and set a configuration for a resolution strategy. This tells gradle to use version 23.0.1 of support-annotations in case there’s a conflict:

1configurations.all {
2      resolutionStrategy.force 'com.android.support:support-annotations:23.0.1' 
3    }
4    
5    // existing android config
6    android {
7      ...
8    }

Running the default test

The final step is to actually run the test:

    detox test

Note that you can also add the -c flag here to only run the test on a specific device. This is especially useful if the machine you’re testing on is not so powerful:

1detox test -c android.emu.debug
2    detox test -c ios.sim.debug

If that doesn’t work and you get an error which looks like this:

rn-dev-tools-testing-shell-error

That means that you need to update the node_modules/detox/src/devices/android/ADB.js file. This file contains all the commands that Detox is executing to manipulate the emulator. Look for the pidof method and replace the first command which looks like this:

1async pidof(deviceId, bundleId) {
2      const processes = await this.shell(deviceId, `ps -AMo NAME,PID`);
3      // rest of the method here...
4    }

With this:

1async pidof(deviceId, bundleId) {
2      const processes = await this.shell(deviceId, `ps`);
3      // rest of the method here...
4    }

We need to do this because certain Android versions. Those that have API version 25 and below doesn’t seem to recognize the ps -AMo NAME,PID command. That’s why putting just ps works.

Save the file and run the test command again. That should solve the issue. This time though, you’ll get another issue:

rn-dev-tools-testing-detox-fail

This one is completely normal because Detox is running the test located at e2e/firstTest.spec.js. If you open it, it should contain three assertions for elements which don’t exist in the app. That’s why it’s failing. We’ll work on this on the Detox in action section. But first, let’s look at how we can work with Jest to implement snapshot testing for the app.

For Windows users who gets an error similar to the following:

rn-dev-tools-testing-detox-windows-issue

This error is due to Windows not being able to properly parse the transfer of commands from Detox. So the solution is to just manually execute the build command you have on the package.json file:

    cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..

If this doesn’t work, try to execute the commands one by one. Also, I recommend you use Git Bash if you keep getting errors.

If you’re having problems getting the above setup to work, I recommend you to go through the following troubleshooting guides first:

Once you’ve exhausted those, search for the specific issue you’re having on the issues page or Stack Overflow.

Jest in action

As mentioned earlier, a React Native project already comes with a test which you can readily execute. Go ahead and try running it while on the root directory of the project:

    yarn test

That should give you the following output:

rn-dev-tools-testing-jest-first-run

This means that the test has passed. By default, it will pass. But as you make changes to your codebase, there will be some unexpected changes. That’s what Jest is meant to test, unintentional changes which break the standard that you’ve initially set for the code.

Solving asset transform issue

Note that if you have an issue which looks like the following. It means that the image is being treated as a JavaScript module instead of an asset:

rn-dev-tools-testing-jest-transform

To solve this, create a fileTransformer.js file at the root of the project directory and put the following. This returns the path of the asset, in this case, an image. Instead of its source:

1// fileTransfomer.js
2    const path = require('path');
3    
4    module.exports = {
5      process(src, filename, config, options) {
6        return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
7      },
8    };

Then in the package.json file, update the jest config to use the fileTransformer.js file for transforming the assets used in the project (I’ve gone ahead and included every image file extension, although we’re only using .jpg in this project):

1"jest": {
2      "preset": "react-native",
3      "testMatch": [
4        "<rootDir>/__tests__/*"
5      ],
6      // add the following:
7      "transform": {
8        "\\.(jpg|jpeg|png|gif)$": "<rootDir>/fileTransformer.js"
9      }
10    },
11    // the rest of package.json

Once you’ve added that, the tests should now run.

Before we write some tests for the app, let’s first try out if Jest is really working as expected. Since we already ran the test, Jest should have stored a snapshot of that file inside the _tests_/_snapshots_ directory. The filename should be the same as the name of the test, but with an additional .snap extension. So for the default test, there should be an App.js.snap file. If you open it, you should see the render tree of the App component.

Let’s prove that the test will fail if we make a change to any child of the App component. Open the src/components/PokemonLoader.js and set the background color:

1const styles = {
2      textContainer: {
3        flex: 1,
4        alignItems: "center",
5        backgroundColor: "#fff", // add this
6      },
7      // the rest of the styles here...
8    }

Then on the _tests_/App.js file, replace the default test with the following:

1it("App is rendered correctly", () => {
2      const tree = renderer.create(<App />).toJSON();
3      expect(tree).toMatchSnapshot();
4    });

The code above checks if the render tree of the App component matches the one in the App.js.snap file.

To verify, run the test again with yarn test and you should see an output similar to the following:

rn-dev-tools-testing-jest-failed

As you can see from the above screenshot, the test is failing, and it shows exactly which specific line caused the failure.

If the change you’ve introduced is correct, you can update the snapshot by adding the -u flag to the command:

    yarn test -u

This updates the .snap file to add the backgroundColor: "#fff``" style declaration. At this point, you can then commit both the component and snap file to version control.

Adding a test

Now it’s time for us to add a test. But before we do that, it’s important to have an understanding of how the app works. So in case you haven’t tried it yet, now is the perfect time to run the app:

1react-native run-android
2    react-native run-ios

Try it out, and take note of how it works. Then think of how you can say the app is working correctly.

Since this is just a tiny app, we’re going to add the test inside the _tests_/App.js file. For larger apps though, you might want to have a separate file for each page of the app, or group the tests based on related functionality.

The first and last thing that we’re going to test is whether the Card component renders correctly and consistently, given the same data:

1import Card from "../src/components/Card"; // the Card component from the app
2    import pokemonData from "./data/pokemon.json"; // the data to be used by the card
3    
4    it("card renders correctly", () => {
5      const tree = renderer
6        .create(<Card fetching={false} data={pokemonData} />)
7        .toJSON();
8      expect(tree).toMatchSnapshot();
9    });

As you can see, this follows the same structure as the sample test from earlier. The only difference is that we’re testing a specific component that’s rendered inside the main one. Also, we’re hardcoding the props that are normally passed in through mapStateToProps.

You can find the contents for the pokemon.json file here. Place it inside the _tests_/data folder.

Other parts of the app which you might want to test are the reducers. To verify if the way they specify how the state will change is correct, given a specific action.

You can also interact with the components (for example: click a button, enter a text on a text field) and verify if their state is updated correctly. I’ll be leaving some links in the last section for further reading.

Detox in action

Detox is used for automating the tests that real users would normally do. If you’re coming from a web development background, Detox’s counterpart is Selenium. The workflow looks something like this:

  • Write code that will automate user interactions. Things like clicking a button, or typing some text in a text field.
  • Write code that will verify that the desired effect of that action is met. For example, when a user clicks a button, you want a specific text to show on the screen.
  • Run the tests to verify the functionality.

Adding the tests

Now we’re ready to write some tests. This time, we’re going to test the following:

  • Verify if the Pokeball image is rendered on the screen when the app is first loaded.
  • Verify if the relevant Pokemon data is rendered on the screen after the user clicks on the button for loading a new Pokemon.

The tests are inside the e2e directory. As mentioned earlier, there’s already a test file (e2e/firstTest.spec.js) created for you. Rename the file to pokemonLoadingTest.spec.js, open it and change the description:

1describe("Pokemon data is loading", () => {
2      // rest of the code here...
3    });

The name of the file and the description should match the specific functionality (or group of functionalities) that you want to test. In this case, we’ll simply include all the tests in a single file since the functionality we’re testing is all related.

The first bit of code you will see after that is telling Detox to reload the React Native app. Note that this does not have a similar effect to when you reload the app using the ⌘ + r, rr, or Ctrl + r on the Android or iOS simulator. So if you made some changes to the code, it wouldn’t actually trigger the watcher to recompile and update the app for you. This method runs before running any of the tests that will be included in this file:

1beforeEach(async () => {
2      await device.reloadReactNative();
3    });

Now, let’s add the first test. That is, to verify if the Pokeball image is being rendered. Because that’s the only visible element which the user can click when the app is first loaded. The easiest way to target it is by adding a testID prop. Use a unique ID that describes what the element is:

1// src/components/PokemonLoader.js
2    {!pokemon && (
3      <Image
4        source={pokeball}
5        resizeMode={"contain"}
6        style={styles.pokeBall}
7        testID="pokeball_image"
8      />
9    )}

Then in the test file, add the test for verifying if the component with that specific ID is being rendered and is visible on the screen. Note that if the component is under the non-visible area of the screen, the test will fail even if it’s rendered:

1// e2e/pokemonLoadingTest.spec.js
2    
3    beforeEach(...);
4    
5    // add this
6    it("should show Pokeball image on app load", async () => {
7      await expect(element(by.id("pokeball_image"))).toBeVisible(); // 75% of the tested component should be visible on the screen
8    });

At this point, you can already run the test:

1react-native start # run the watcher
2    detox test -c android.emu.debug # or ios.sim.debug

Note that we have to run the watcher first before running the test. Since we’re in debug mode, the app wouldn’t actually work if the watcher isn’t running. Also, if you’re testing on iOS, running the test doesn’t automatically open the iOS simulators UI (although it’s already running in the background). So if you want to see the actions being performed, you have to execute open -a Simulator on your terminal.

Once that’s done, Detox should uninstall the app (if it already exists), re-install it, launch it, and then run the tests. The result should look something like this:

rn-dev-tools-testing-jest-passed

Since we’re in debug mode, and any changes you make to the code will get recompiled by the watcher, you can actually just reuse the installed app. This bypasses the uninstalling and re-installing of the app and immediately proceeds with launching it and running the tests:

    detox test -c android.emu.debug --reuse

Though if you make changes to the code, don’t forget to reload the app so the watcher recompiles the code.

Let’s proceed with the final test. In this test, we want to verify whether all the relevant Pokemon data shows up in the screen when the user clicks the button for loading a new Pokemon. Update the src/components/Card.js and add a testID to the components which display all the relevant data. This includes the sprite, name, types, and description of the Pokemon:

1<Image
2      style={styles.cardImage}
3      source={{ uri: image }}
4      resizeMode={"cover"}
5      testID="pokemon_sprite"
6    />
7    ...
8    <Text style={styles.title} testID="pokemon_name">
9      {name}
10    </Text>
11    ...
12    <View
13      style={styles.typesContainer}
14      testID="pokemon_types"
15    >
16      {this.renderTypes(types)}
17    </View>
18    ...
19    <Text
20      style={styles.description}
21      testID="pokemon_description"
22    >
23      {description}
24    </Text>

You also need to update src/components/PokemonLoader.js to add a testID to the button for loading the Pokemon:

1<TouchableOpacity
2      onPress={requestPokemon}
3      testID="action_button"
4    >

Next, add the test to the test file:

1// e2e/pokemonLoadingTest.spec.js
2    it("should show relevant Pokemon data after clicking the button and loading the data from the API", async () => {
3      await element(by.id("action_button")).tap(); // click the button
4      
5      // verify that the components showing the relevant data exists
6      await expect(element(by.id("pokemon_sprite"))).toExist();
7      await expect(element(by.id("pokemon_name"))).toExist();
8      await expect(element(by.id("pokemon_types"))).toExist();
9      await expect(element(by.id("pokemon_description"))).toExist();
10    });

From the code above, you can see how simply it performs actions on a specific element. After the action is performed, we can immediately verify if the expected components are rendered in the screen. Previously, we’ve used toBeVisible(), this time, we’re using toExist() instead. This is a more relaxed way of checking whether the component exists. As the name suggests, the component doesn’t have to be visible in the screen for the test to pass. All it verifies is whether it is rendered or not.

Detox does all the magic required to keep the tests and the current state of the app in sync. What this means is that Detox knows when the app is busy making a network request, or any other non-trivial processing. Then once that process is done, that’s the only time that it runs the test.

Check out the following docs to learn more:

  • Matchers - ways for targetting a component.
  • Expectations - assertions.
  • Actions - actions and user gestures that can be performed on a component.

Further reading

Here are some resources for your continued journey on testing React Native apps:

Conclusion

That’s it! In this tutorial, you’ve learned how to test your React Native apps. Specifically, you’ve learned how to use Jest and Detox to implement snapshot and end-to-end testing.

As you have seen, Jest is a developer-ready tool while Detox is still getting there, especially on Android. Though, that shouldn’t stop you from using the tool because it’s continually being developed.

You can view the full source code used in this tutorial on its GitHub repo.

That wraps up the series! In this series, you’ve learned how to lint, debug, and test your React Native project. I hope you’ve gained the necessary skills for improving your React Native code by means of tooling.