Testing React Native Apps eBook by Codemagic
Testing React Native Apps eBook by Codemagic
React Native
Apps
by Nevercode
Testing
React Native Apps
by Codemagic and Sneh Pandya
All rights reserved. This publication text may not be uploaded or posted online without the prior
written permission of the publisher. For permission requests, write to the publisher, addressed
“Ebook permissions request”: liina@nevercode.io
Introduction 2
Conclusion 59
codemagic.io 1
Introduction
Mobile apps play a crucial role in people’s lives and, as a result, directly impact
businesses. The bar is set high for all mobile apps in the market. By the end of 2021,
there will be roughly seven billion mobile users worldwide, according to a survey by
Statista. This exponential growth in the adoption of mobile apps can be attributed to
the increasing popularity of hybrid or cross-platform apps over native platforms.
Since 2015, React Native has consistently gained popularity and emerged as a popular
choice in the cross-platform mobile application space, as it saves between 40 and 90
percent of time spent developing apps on different platforms.
There were 218 billion mobile app downloads in 2020. That’s a seven percent increase
year over year, according to App Annie, an independent mobile statistics and analytics
research firm. Developing and maintaining an efficient app is a complicated process
and requires the investment of a significant amount of time and money.
Improvements, periodic updates and fixes are critical for the overall success of an app
in the long run.
According to Google, 25 percent of mobile users abandon a product after only one use,
whereas a whopping 70 percent of users abandon an app because it takes too long to
load. As an application grows and the codebase expands, small errors and edge cases
you don’t expect can cascade into larger failures.
Any mobile app needs to be thoroughly tested. Performance factors like loading times,
crashes, and complicated input fields all add up to a poor user experience and can ruin
the chances of market success. According to Instabug, 67 percent of bugs are reported
on iOS devices only. Bugs lead to bad user experiences and, ultimately, business losses.
One way to prevent fragile programming is to test your code thoroughly before
releasing it into the wild.
This ebook acts as a getting-started primer on the world of testing for apps built using
the React Native framework. The topics in this ebook cover a wide variety of
fundamental concepts and how-tos.
codemagic.io 2
React Native testing: Types of tests
There are multiple types of tests for applications that ensure a sophisticated approach
and confidence in delivering apps to the end users. But what are the differences
between them? Let’s find out!
The drastically shortened feedback loop supercharged by automated tests goes hand
in hand with continuous integration, continuous delivery and DevOps culture. If you
want to get serious about automated tests for your apps, you need to know the concept
of the test pyramid. Mike Cohn came up with this concept in his book Succeeding with
Agile.
codemagic.io 3
The three primary types of layers in the testing pyramid are:
● Unit tests: Unit testing ensures that each part of the code delivers the desired
output. Developers only look at the interface and the specifications of the
components. Thorough standalone testing of each component is performed
before moving on to the next unit.
The foundation of your test suite will be made up of unit tests. Your unit tests
make sure that a certain unit (the subject being tested) of your codebase works
as intended. Unit tests have the narrowest scope of all the tests in your test
suite. The number of unit tests in your test suite will largely outnumber any
other type of test.
All non-trivial applications will integrate with some other parts (e.g., databases,
filesystems, network calls to other applications). When writing unit tests, these are
usually the parts you leave out in order to come up with better isolation and faster
tests. Still, your application will interact with other parts, and these interactions need
to be tested. Integration tests are there to help. They test the integration of your
application with all the parts that live outside of your application.
codemagic.io 4
Unit testing for React Native
By default, React Native provides Jest, a framework for unit testing that works for both
Android and iOS. Currently, test coverage isn’t perfect, but according to Facebook,
more unit-testing capabilities will be introduced in React Native, and users can
already build their own.
Every test case starts from a describe() function call, similar to how JUnit uses the
TestCase class. The describe() function takes two parameters: the title description and
the function to be executed. The it() function includes all of the test steps and returns
a series of expect() functions.
Below is an example:
describe("Music", function() {
var music;
var song;
beforeEach(function() {
music = new Music();
song = new Song();
});
codemagic.io 5
// demonstrates use of 'not' with a custom matcher
expect(music).not.toBePlaying(song);
});
music.play(song);
music.makeFavorite();
expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
});
expect(function() {
music.resume();
}).toThrow("song is already being played");
});
});
});
This example shows how Jasmine can be used to test the functionality, but it keeps the
focus on method-level testing. Also, the React Native framework provides extra
capabilities for testing integrated components. This works for both native and
JavaScript components, enabling seamless communication between them via a bridge.
codemagic.io 6
Testing React Native apps with Jest and Codemagic
Jest is a testing framework built using JavaScript and maintained by Facebook.
Facebook itself uses the Jest testing framework to test its own React Native apps.
For React Native version 0.38 and above, Jest is included when you create a new
project, so the following setup may already be completed for you.
Before testing a React Native app with Jest, let’s start by configuring the React Native
project.
"scripts": {
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"setupFiles": ["<rootDir>/testing/jest-setup.js"]
}
Let’s say your app has one stack navigator and two screens. Your app would look
something like this:
codemagic.io 7
export const Screen1 = ({ navigation }) => (
<View style={styles.container}>
<Text>Screen 1</Text>
<Button title="Go to Screen 2" onPress={() =>
navigation.push('Screen2')} />
<StatusBar style="auto" />
</View>
);
...
...
codemagic.io 8
React Native testing: Writing React Native integration tests
Let’s write our integration test. You don’t need to know how the app is implemented
since you want to test the app from a black-box testing perspective. Here’s what you
need to know:
● Screen1 is our initial screen in the app, which has a navigation function in
place.
● The user should be able to navigate to Screen2 when the button is pressed.
describe('Screen1', () => {
it('navigates on button press', () => {
const push = jest.fn();
const { getByText } = render(<Screen1 navigation={{ push }} />);
fireEvent.press(getByText('Go to Screen 2'));
expect(push).toHaveBeenCalledWith('Screen2');
});
});
Pretty straightforward test, right? The only thing specific to React Navigation that we
had to implement here is passing a navigation property to the component when we
render it.
Next, we will create a testing setup file called testing/jest-setup.js:
import 'react-native-gesture-handler/jestSetup';
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
Reanimated.default.call = () => {};
codemagic.io 9
return Reanimated;
});
In your terminal, simply run npm test to run the test for React Navigation with Jest
that you just wrote.
PASS ./App.test.js
Screen 1
✓ navigates on button press (40ms)
AppStack
✓ renders the correct screen (32ms)
Codemagic is a popular CI/CD solution for mobile apps. One of its coolest features is
running automated tests whenever a new commit or pull request is pushed in your Git
repository. To set up your React Native app on Codemagic, you can follow the steps
provided here.
codemagic.io 10
Once the project setup has been completed on Codemagic, you need to add the testing
configuration in the codemagic.yaml file, as shown below, for the tests to be executed:
workflows:
...
scripts:
- name: Install npm dependencies
script: npm install
...
...
...
Once codemagic.yaml is updated, you are ready to run your tests and build on
Codemagic.
Codemagic can be configured to start the build process automatically after a commit
or a pull request using build triggers. For now, we will trigger the build manually to
start the tests and build process.
Just click on the Start new build button to trigger your build. Now, wait for the build to
complete, and it will show you the step results, including the test run.
Jest is a popular choice when writing test cases for React Native. You can learn more
about React Native testing with Jest here.
codemagic.io 11
Testing custom views and components in React
Native
Nowadays, every app has unique elements, including UI, UX and even functional
perspectives. To keep the experience robust and smooth, it is essential to test even the
tiniest components, especially those with custom behaviors. Let’s dive into testing
custom reusable components in React Native.
Let’s get started by setting up our project. To create a new application, run the
commands as follows:
cd ReactNativeComponentTesting
Let’s install the dependencies by running the command below in the terminal:
codemagic.io 12
Create a reusable custom component
Let’s write a reusable React Native Button and test it. Suppose the reusable Button
component contains the specifications below:
We also need to add property controls to this button for greater usability, flexibility
and unplanned changes. The newly added properties would be:
// property declaration
const { title, onPress, primary, secondary, height, width } = props;
return (
<TouchableOpacity onPress={onPress}>
<Text>{title}</Text>
</TouchableOpacity>
);
}
Now, with the default function in place, let’s add styling and different variants.
codemagic.io 13
export default (Button = props => {
// added conditions
const HEIGHT = large ? 60 : height ? height : 40;
const WIDTH = width ? width : 200;
const BACKGROUND_COLOR = secondary ? 'red' : baseColor ? baseColor :
'blue';
const TEXT_COLOR = textColor ? textColor : 'white';
return (
<TouchableOpacity
// added styling
style={{
alignItems: 'center',
justifyContent: 'center',
backgroundColor: BACKGROUND_COLOR,
height: HEIGHT,
width: WIDTH,
}}
onPress={onPress}>
<Text style={{color: TEXT_COLOR}}>{title}</Text>
</TouchableOpacity>
);
});
The above code snippet has added property declarations and conditions. Let’s look at
them in detail below:
● For height: If large is true, then set HEIGHT to 60. If height is true, then set
HEIGHT as height, or else set height to 40.
● For width: If width is true, then set width as width, or else set width to 200.
codemagic.io 14
● For BACKGROUND_COLOR: If secondary is true, then set BACKGROUND_COLOR to
red. If baseColor is true, then set BACKGROUND_COLOR as baseColor, or else set
BACKGROUND_COLOR to blue.
With the growing number of properties and features in the application, even simple
reusable components like Button can become complicated as a result of mutations and
possible combinations. Let’s have a look at how we would use this component in our
app:
codemagic.io 15
{/* secondary button */}
<Text>Secondary Large</Text>
<Button title="Test Button" secondary large />
It’s time to set up the testing configuration for our code. We have already installed all
the required dependencies while setting up the application. Create a new file called
Button.test.js, and add the test case as shown below:
import 'react-native';
import React from 'react';
import renderer from 'react-test-renderer';
codemagic.io 16
const wrapper = renderer.create(<Button title="Test Button" />);
At the top of the file, the renderer is imported from react-test-renderer, which
provides a container (or wrapper) for our custom component.
describe marks the start of a new test suite in Jest. The first argument is a String
containing a description of what the encompassing test suite is testing. The second
argument is a callback function where the test execution steps are written.
it defines the start of a new test case in Jest. Tests should be as small and concise as
possible, and they should only test one thing. The arguments are the same as those of
describe – a String defining the test scenario and a callback function.
The .toJSON() and the .toBeTruthy() assertion function provided by Jest are
written inside of the it block. This will check if the value is not null or undefined.
To test the custom reusable Button, the different use cases can be:
● Primary button: height 40, width 200, baseColor blue, textColor white
● Secondary button: height 40, width 200, baseColor red, textColor white
● Custom baseColor: can be applied to both the primary and secondary button
● Custom textColor: can be applied to both the primary and secondary button
Below is the test suite for the primary button. For brevity, the test suite of the
secondary button is skipped because the methods for the secondary button are the
same.
import 'react-native';
import React from 'react';
codemagic.io 17
import renderer from 'react-test-renderer';
console.log(wrapper.toJSON())
Let’s say we want to test that our textColor is what we expected. The line
console.log(wrapper.toJSON()) gives the output shown below. This is useful for
testing our style and other elements, like children.
codemagic.io 18
{
type: 'View',
props: {
accessible: true,
style: {
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'blue',
height: 40,
width: 200,
opacity: 1
}
...
children: [ { type: 'Text', props: [Object], children: [Array] } ]
}
}
Let’s look at the next example of testing that the textColor is specified as we
expected. In the log above, children is an array of all the children seen from the root
node in the render tree. Moreover, there is only one child in this case, and the styles
can be fetched from children with the line of code below:
console.log('Children styles:=>',
wrapper.toJSON().children[0].props.style);
children[0]returns the first object in the array, and props.style returns the style
object. The output would look like this:
Since the structure becomes clearer with the above output, we can write the
declaration and the same type of test case as shown above:
// declaration
const { color: buttonTextColor } = childStyles;
// test case
it('Should have white text', () => {
expect(buttonTextColor).toBe('white');
codemagic.io 19
});
Once these techniques are understood, writing tests using Jest and React Native
becomes easier for all the other combinations of custom reusable Buttons. Run the
test using the command below:
Voila! The tests are written and successfully tested, with all tests passing!
codemagic.io 20
Functional testing for React Native
<Radio onSelect={this.onSelect.bind(this)}
defaultSelect={this.state.optionSelected - 1}>
<Option color="blue" selectedColor="#0000FF">
<Item title="First option" description="First radio button"/>
</Option>
<Option color="blue" selectedColor="#0000FF">
<Item title="Second option" description="Second radio button"/>
</Option>
<Option color="blue" selectedColor="#0000FF">
<Item title="Third option" description="Third radio button"/>
</Option>
</Radio>
The test snippets included for each framework section explain how the scripts deal
with UI elements and how clicks and other user inputs are handled. The purpose of the
examples is to compare and show the different options that are available for test
automation and what programming languages can be used for testing.
codemagic.io 21
React Native testing using Appium
Let’s learn how Appium can be used for React Native testing.
To begin, create a new project for React Native using the command below:
Next, create a separate directory to write test cases. Ideally, it should be inside the
project’s root folder to keep everything in place. Inside the tests folder, create a
package.json file.
Next, install ChaiJS as shown below to include support for assertion statements while
writing test cases:
codemagic.io 22
Configure WebdriverIO
The web driver config file needs to be generated to apply the configuration while
testing. Run the command below inside the project:
In return, the command prompt will ask a series of questions and install the required
dependencies based on the input selections.
Note: You need to select both the Mocha and Appium services when configuring via
the command line.
Once this is done, the wdio.conf file will be generated inside the tests directory.
Perform the following changes to configure WebdriverIO to work with Appium and
run tests on Android Emulator:
exports.config = {
services: ['appium'],
port: 4723,
runner: 'local',
specs: [
'./tests/specs/**/*.js'
],
capabilities: [{
maxInstances: 1,
browserName: '',
appiumVersion: '1.13.0',
platformName: 'Android',
platformVersion: '<emulator platform version>',
deviceName: '<emulator name>',
app: '<path to APK>',
automationName: 'UiAutomator2'
}],
logLevel: 'trace',
bail: 0,
waitforTimeout: 10000,
connectionRetryTimeout: 90000,
codemagic.io 23
connectionRetryCount: 3,
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd'
timeout: 60000
}
}
In this file, Appium is added to the service list, and the default port for communication
is also set. Declare the APK path, Android emulator name and version to complete the
configuration. Also, maxInstances is set to 1 to avoid running multiple tests in
parallel.
Let’s prepare a simple application for the purpose of React Native testing using
Appium. In our case, we will add a simple login page to run tests using Appium. Inside
the App.js file, add the code below:
codemagic.io 24
login = () => {
if ((this.state.username == 'codemagic') && (this.state.password ==
'nevercode')) {
this.setState({ isLogined: true });
}
else {
this.setState({ isLogined: false });
}
}
render() {
return (
<View style={LOCAL_STYLES.wrapper} testID="app-root"
accessibilityLabel="app-root">
<View style={LOCAL_STYLES.inputContainer}>
<TextInput name="username" accessibilityLabel="username"
style={LOCAL_STYLES.input} onChangeText={(text) =>
this.inputChangeHandler(text, "username")} />
</View>
<View style={LOCAL_STYLES.inputContainer}>
<TextInput name="password" accessibilityLabel="password"
secureTextEntry={true} style={LOCAL_STYLES.input} onChangeText={(text) =>
this.inputChangeHandler(text, "password")} />
</View>
<Text accessibilityLabel="loginstatus">{this.state.isLogined ?
"success" : "fail"}</Text>
<TouchableHighlight style={LOCAL_STYLES.buttonContainer}
accessibilityLabel="login" onPress={this.login}>
<Text style={{ color: 'white' }}>Login</Text>
</TouchableHighlight>
</View>
);
}
}
codemagic.io 25
justifyContent: "center"
},
inputContainer: {
borderBottomColor: '#AFAFAF',
backgroundColor: '#FFFFFF',
borderRadius: 10,
borderBottomWidth: 1,
marginBottom: 16,
flexDirection: 'row',
alignItems: 'center',
width: '80%',
borderColor: 'blue',
borderWidth: 1
},
buttonContainer: {
height: 45,
width: 250,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
borderRadius: 20,
backgroundColor: "#00b5ec"
}
});
To make the components testable, add a property to the relevant components. For
Android, add accessibilityLabel, and for iOS, add testID, as shown in the code
above.
Now run the app and check the screen. A simple login screen should open up. Next,
let’s write test cases using Appium to verify our login functionality.
Inside the tests directory, create a new file named App.test.js, and add the code
below:
codemagic.io 26
var expect = require('chai').expect;
beforeEach(() => {
$("~app-root").waitForDisplayed(10000, false)
});
$("~login").click();
$("~loginstatus").waitForDisplayed(11000);
const status = $("~loginstatus").getText();
expect(status).to.equal('success');
});
$("~login").click();
$("~loginstatus").waitForDisplayed(11000);
const status = $("~loginstatus").getText();
expect(status).to.equal('fail');
});
});
The above test case will verify the login functionality and check whether the test
passes or fails based on the input credentials. beforeEach is added to make sure the
app is loaded prior to the test being run.
Execute tests
To execute the test case written above, let’s launch an emulator with the command
below:
codemagic.io 27
react-native run-android
Now, let’s start the Appium server in a separate command prompt to set up
communication:
appium
In a new command prompt, run the WebDriver test suite using the command below:
Once set up, the test execution will start, and the command prompt will show the logs
of the executed steps in parallel when the emulator shows the test running.
Once the automation process is complete, you will see the test results as follows:
App testing
✓ Login test: valid case
✓ Login test: invalid case
It is popular in the developer ecosystem to use Appium to test React Native apps.
Appium makes it seamless to write test cases for both the Android and iOS platforms
while working with React Native. Selenium, the underlying web driver, works as a
bridge between Appium and mobile platforms to deploy and execute tests.
codemagic.io 28
I wait for text "What is the best way to test a mobile application on a
thousand devices?"
Then I press the radio button 0
Then I press the radio button 1
Then I press the radio button 2
Then I enter the text "Sample Test" into field with id "editText1"
Then I press view with id "Button1"
Steps usually begin with one of the following keywords: given, then, when, and or but.
onView(withId(R.id.radio0)).perform(click());
onView(withId(R.id.radio1)).perform(click());
onView(withId(R.id.radio2)).perform(click());
onView(withId(R.id.EditText1)).perform(click());
onView(withId(getInstrumentation().getTargetContext().getResources()
.getIdentifier("com.example.app:id/edittext1", null,
null))).perform((typeText("Simple Test")));
onView(withId(getInstrumentation().getTargetContext().getResources()
.getIdentifier("com.example.app:id/button1", null,
null))).perform(click());
Espresso is a powerful framework that offers many additional services and function
calls to developers.
codemagic.io 29
React Native testing using XCTest for iOS
XCTest comes with Xcode but can also be used with both real iOS devices and
simulators. It allows developers to write tests for components at any level and also
provides a framework for UI testing capabilities. XCTest tests are grouped into
subclasses of XCTestCase. Writing tests using XCTest is trivial because it is fully
compatible with both Objective-C and Swift.
- (void)testClicksOnRadioButtons {
[tester tapViewWithAccessibilityLabel:@"Radio1"];
[tester tapViewWithAccessibilityLabel:@"Radio2"];
[tester tapViewWithAccessibilityLabel:@"Radio3"];
[tester tapViewWithAccessibilityLabel:@"Answer"];
}
testClicksOnRadioButtons() {
let app = XCUIApplication()
app.radiobutton[0].tap()
app.radiobutton[1].tap()
app.radiobutton[2].tap()
app.staticTexts["Simple Test"]
app.button[0].tap()
}
codemagic.io 30
How to run React Native Detox tests on Codemagic
The most difficult part of automated testing on mobile is performing end-to-end
(E2E) tests. These involve testing the app from the user’s perspective by running it on
a device (or emulator/simulator).
One of the major issues with other black-box testing libraries is that tests are usually
non-deterministic and flaky. We will take a look at one of the most popular
end-to-end testing libraries for React Native, called Detox.
You can use the following command to create a new React Native app:
Let’s install the dependencies needed to set up Detox for your React Native project in
order to start writing Detox tests. You can do this with the commands below:
Navigate to your React Native root project directory, and run the following:
codemagic.io 31
Configuring for Android
allprojects {
repositories {
maven {
// All of the Detox artifacts are provided via the npm module
url "$rootDir/../node_modules/detox/Detox-android"
}
}
}
android {
defaultConfig {
// Added these for running tests
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
}
dependencies {
// Added testing dependencies
androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12'
}
package [your_package];
codemagic.io 32
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new
ActivityTestRule<>(MainActivity.class, false, false);
@Test
public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
detoxConfig.rnContextLoadTimeoutSec =
(com.[your_package].BuildConfig.DEBUG ? 180 : 60);
Detox.runTests(mActivityRule, detoxConfig);
}
}
You have to add configurations for using the detox-cli commands for building and
testing the app. Add the following to the .detoxrc.json file:
{
"testRunner": "jest",
"runnerConfig": "e2e/config.json",
"configurations": {
codemagic.io 33
"android.emu.debug": {
"type": "android.emulator",
"binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
"build": "cd android && ./gradlew assembleDebug assembleAndroidTest
-DtestBuildType=debug && cd ..",
"device": {
"avdName": "Pixel_API_29"
}
},
"android.emu.release": {
"binaryPath":
"android/app/build/outputs/apk/release/app-release.apk",
"build": "cd android && ./gradlew assembleRelease assembleAndroidTest
-DtestBuildType=release && cd ..",
"type": "android.emulator",
"device": {
"avdName": "Pixel_API_29"
}
}
}
}
In the above JSON, replace the avdName with the name of the emulator or device that
you are using.
You do not need to add additional dependencies for iOS, so configuring for iOS is
pretty straightforward. Just add the Detox configurations to the .detoxrc.json file:
{
"testRunner": "jest",
"runnerConfig": "e2e/config.json",
"configurations": {
"ios.sim.debug": {
"type": "ios.simulator",
"binaryPath":
"ios/build/Build/Products/Debug-iphonesimulator/hello_detox.app",
codemagic.io 34
"build": "xcodebuild -workspace ios/hello_detox.xcworkspace -scheme
hello_detox -configuration Debug -sdk iphonesimulator -derivedDataPath
ios/build",
"device": {
"type": "iPhone 11"
}
},
"ios.sim.release": {
"binaryPath":
"ios/build/Build/Products/Release-iphonesimulator/hello_detox.app",
"build": "export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace
ios/hello_detox.xcworkspace -UseNewBuildSystem=YES -scheme hello_detox
-configuration Release -sdk iphonesimulator -derivedDataPath ios/build",
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
}
}
}
}
Jest is a recommended test runner for React Native, and we will use it for setting up
the Detox tests in our React Native app. Use the following CLI command provided by
Detox for initializing Jest:
This command will create an e2e folder in the root project directory with sample test
code. We will add a very simple test that checks whether a Text component is present
on the screen.
codemagic.io 35
Now, navigate to e2e/firstTest.e2e.js, and replace its content with the following:
describe('Example', () => {
beforeEach(async () => {
await device.reloadReactNative()
})
With the Detox test environment properly configured, now you are ready to run the
test on your device (or emulator/simulator). First, you have to perform a build of the
app before running the test.
codemagic.io 36
Detox allows React Native apps to be tested with a focus on end-to-end functionality.
Hence, it is a powerful tool that every React Native developer should have under their
belt.
Did you know that Codemagic now supports Linux instances too? Here’s yet another
way to build and test React Native apps!
On Linux, you can build Android and other non-Mac workflows more quickly with
out-of-the-box support for Android emulators with hardware acceleration. You can
specify a Linux build machine by using linux or linux_x2 as the instance type in
codemagic.yaml.
Linux can only be used in codemagic.yaml configured workflows, so see the getting
started guide or YAML cheat sheet to migrate over any workflows you want to use on
Linux.
Let’s look at how to build and test React Native apps on Linux instances.
codemagic.io 37
npm install @testing-library/react-native --save-dev
npm install @react-navigation/native --save-dev
"scripts": {
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"setupFiles": ["<rootDir>/testing/jest-setup.js"]
}
Let’s prepare a simple app that has two screens. Your app would look something like
this:
codemagic.io 38
<Stack.Navigator>
<Stack.Screen name="HomeScreen" component={HomeScreen} />
<Stack.Screen name="DetailScreen" component={DetailScreen} />
</Stack.Navigator>
);
Scenario 1: The Home Screen is the initial screen of the app, which has navigation
enabled. The user is able to navigate to the Detail Screen by tapping on the button.
describe('HomeScreen', () => {
it('navigates on button press', () => {
const push = jest.fn();
const { getByText } = render(<HomeScreen navigation={{ push }} />);
fireEvent.press(getByText('Go to Detail Screen'));
expect(push).toHaveBeenCalledWith('Detail Screen');
});
});
Scenario 2: Let’s say you want to check your Detail Screen for a text box that collects
users’ emails for a newsletter. Jest will test the selected component in JSON format.
expect(findTextElement(tree, 'email')).toBeDefined();
})
codemagic.io 39
Running tests for React Native
import 'react-native-gesture-handler/jestSetup';
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
Reanimated.default.call = () => {};
return Reanimated;
});
Testing on a local machine is simple – just go to your terminal and simply run npm
test. This will run the tests you just implemented. You should be able to see an output
similar to this:
PASS ./App.test.js
Home Screen
✓ navigates on button press (40ms)
AppStack
✓ renders the correct screen (32ms)
Detail Screen
✓ email field found (94ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.62s, estimated 2s
Ran all test suites related to changed files.
codemagic.io 40
Running tests on Codemagic
Codemagic is a popular CI/CD solution that offers automated build pipelines to check
continuous integration and run tests. To set up your React Native app on Codemagic,
you can follow the steps provided here.
This time, we will build and test our React Native app on a newly available Linux
instance. Let’s set up a codemagic.yaml file to perform this.
workflows:
my-workflow:
name: My workflow name
instance_type: linux # specify linux or linux_2 for
Linux instances
max_build_duration: 60
...
scripts:
- name: Install npm dependencies
script: npm install
...
...
...
Once codemagic.yaml is updated, you are ready to run your tests and build on
Codemagic.
codemagic.io 41
Codemagic can be configured to start the build process automatically after a commit
or a pull request using build triggers. For now, we will trigger the build manually to
start the tests and build process.
Just click on the “Start new build” button to trigger your build. Now, wait for the build
to complete, and it will show you the step results, including the test run.
This is how you can build a React Native app on Codemagic’s Linux instance as well as
test your app. Codemagic provides simple, straightforward, out-of-the-box solutions
to achieve your tasks of building, testing and publishing apps without hassle.
codemagic.io 42
Integration testing for React Native
While performing React Native testing via integration tests, the communication goes
through the bridge and requires both native and JavaScript components to be in place.
Cavy is a test framework for React Native that is cross-platform and supports
integration tests.
Cavy makes use of React ref generating functions to provide the ability to refer to and
simulate actions for deeply nested components within an app. Cavy can run
seamlessly on a host device (e.g., Android or iOS simulator).
To get started, install Cavy using yarn:
or npm:
For your app, you need to import Tester, TestHookStore and the specs in a top-level
JavaScript file, as shown below:
codemagic.io 43
Then the components need to be added by adding references and using the
generateTestHook() function. generateTestHook() takes a string as its first
argument and an optional second argument to set your own ref generating function.
Functional components cannot be assigned a ref since they do not hold any instances.
Now, we can write the spec functions referencing the already hooked components.
Then register the AppWrapper as the main entry point with AppRegistry instead of the
current App component, as shown below:
codemagic.io 44
Below is the available set of spec helpers:
// Fills in the identified component with the string. The component must
respond to onChangeText.
fillIn(identifier, str)
// Pauses the test for this length of time in milliseconds. Useful if you
need to allow time for a response to be received before progressing.
pause(integer)
A good alternative for running unit tests and integration tests is Mocha, which
provides a feature-rich JavaScript test framework that runs on Node.js.
codemagic.io 45
Testing local databases for React Native
One of the main goals of testing such components is to catch bugs earlier by running
the tests locally or through a process. Unit tests for components like databases can
help developers who may not have worked on the code to understand what it should be
doing when it executes.
In native Android, it’s common to test SQLite code in isolation. Let’s see how it is
possible to do this for React Native using the Jest testing framework.
Let’s get started by setting up our project. To create a new application, run the
commands as follows:
Next, we will use node-sqlite3 to test sqlite code using jest. It is compatible with
packages like react-native-sqlite2 and react-native-sqlite-storage.
Let’s install the dependencies by running the command below in the terminal:
codemagic.io 46
Make sure you are using the same SQLite version in both Jest and the application
environment.
Once successfully completed, you can verify that Jest has been added in the
package.json file with the configuration:
{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "react-native"
}
}
To use Jest, we need to follow a few steps to set up Jest in our React Native
application.
1. In the root directory of the project, add a new file and name it jest.config.js.
2. Within jest.config.js, add the code below:
module.exports = {
preset: 'react-native',
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
};
preset is a preset that is used as a base for Jest’s configuration. A preset should point
to an npm module that has a jest-preset.json or jest-preset.js file at the root.
codemagic.io 47
Define queries for tests
We will extract a query executor from the application’s environment for testing
purposes.
Now, let’s define queries using the read and write of the executor in
createUsers.ts, as shown below:
codemagic.io 48
The executor returns a Promise with the result of the read and write query operations.
Next, let’s add the test configuration for the database, as shown below:
describe('usersQueries', () => {
beforeEach(async () => {
await userQueries.createTable();
})
it('getCount should return zero for empty table', async () => {
const count = await userQueries.getCount();
expect(count).toEqual(0);
});
});
This test will describe the status of the Users table after communicating with the
actual local database.
After writing the test for our database, we will implement SqlExtractor in our React
Native application.
codemagic.io 49
const executeSql = (
tr: SQLTransaction,
query: string,
...args: string[]
):Promise<string[]> => new Promise((resolve, reject) => {
tr.executeSql(
query,
args,
(_, result) => resolve(parseSql(result)),
(_, error) => {
reject(error);
return true;
},
);
});
Along with React Native’s growing adoption, the importance of the stability of apps
built using React Native is also growing. The React Native framework provides testing
methods, but none of them thoroughly cover the actual logic written in the apps.
Therefore, React Native apps benefit more from functional testing.
codemagic.io 50
API calls are a crucial part of any application since the majority of functionalities rely
on network requests nowadays. Let’s look into how to perform functional testing for
APIs.
We will proceed with Jest, Enzyme and Axios Mock Adapter for this particular article
to test API calls in our React Native application. Jest and Enzyme act as the backbone
of the testing stack. We will use Enzyme to do our setup and simulate runtime
behavior. We will use axios-mock-adapter to mock our API calls in order to test them.
Let’s get started by setting up our project. React Native creates directories for both
Android and iOS that are labeled android and ios, respectively. To create a new
application, run the commands as follows:
cd ReactNativeAPITesting
The dependencies required for the project setup are shown below. Copy them into your
package.json file:
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"collectCoverage": true,
codemagic.io 51
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react
-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navig
ation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
],
"setupFilesAfterEnv": [
"<rootDir>src/setupTests.js"
],
"collectCoverageFrom": [
"**/*.{js,jsx}",
"!**/coverage/**",
"!**/node_modules/**",
"!**/babel.config.js",
"!**/jest.setup.js"
]
},
"dependencies": {
"@react-native-community/hooks": "^2.5.1",
"@react-native-community/masked-view": "0.1.6",
"@react-navigation/bottom-tabs": "^5.7.3",
"@react-navigation/native": "5",
"@react-navigation/stack": "^5.7.1",
"apisauce": "^1.1.2",
"babel-jest": "^26.3.0",
"babel-preset-react-native": "^4.0.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.4",
"expo": "~37.0.3",
"expo-cli": "^3.27.10",
"expo-constants": "^9.0.0",
"expo-image-picker": "~8.1.0",
"expo-location": "~8.1.0",
"expo-permissions": "~8.1.0",
"faker": "^5.1.0",
"formik": "^2.1.4",
"global": "^4.4.0",
"jest": "^26.4.2",
"jest-react-native": "^18.0.0",
"lottie-react-native": "~2.6.1",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native":
codemagic.io 52
"https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz",
"react-native-gesture-handler": "~1.6.0",
"react-native-reanimated": "~1.7.0",
"react-native-safe-area-context": "0.7.3",
"react-native-screens": "~2.2.0",
"react-native-web": "~0.11.7",
"react-test-renderer": "^16.13.1",
"yup": "^0.29.1"
},
"devDependencies": {
"@babel/core": "^7.8.6",
"babel-preset-expo": "~8.1.0",
"jest-expo": "^38.0.2"
},
"private": true
}
npm install
// OR
yarn install
codemagic.io 53
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: #fff
}
})
export default AppButton;
In this section, we’ll test for user interaction and the UI of the app components by
testing snapshots using Jest. To achieve this, we need to install Jest and its
dependencies:
"scripts": {
"test" "jest"
},
"jest": {
"preset": "jest-expo"
}
"jest": {
"preset": "jest-expo",
codemagic.io 54
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react
-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navig
ation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
]
}
Let’s write a basic test first. Create a file named App.test.js, and add the details as
shown below:
Now run npm test or yarn test in your terminal. If App.js has a single child
element, the test will pass.
To call APIs and test them, we are going to make use of mock functions. Mock
functions are copies of objects or a function without the real workings of that
function. Rather, it imitates that particular function. Mocks mainly reduce the need
for dependencies.
React Native includes fetch in the global object. To avoid making real API calls in our
unit test, we mock them. A way to mock is shown below:
global.fetch = jest.fn();
codemagic.io 55
() => Promise.resolve({json: () => Promise.resolve(JSON.parse(body))})
);
};
The function tries to fetch an API once. Having done this, it returns a promise, and
when it is resolved, it returns the body in JSON. It’s similar to the mock response for a
failed fetch transaction – it returns an error. Below is a component of our app:
Now we will try to test all our components. Directly accessing our database is not a
feasible solution. Instead, we need to mock a component of the product by using Jest
to describe the objects in the component, as shown below:
codemagic.io 56
});
it("contains products quantity", () => {
expect(value).toBe(3);
});
});
Enzyme is used in the above code snippet to perform the full rendering, which actually
mounts the component in the DOM using the mount keyword. This means that tests
can affect each other if they are all using the same DOM. While writing tests, you
should also use munmount or something similar to clean up the DOM if necessary.
Now let’s mock an external API call. To test an external call to an API, you need to
mock requests as well as manage the responses. First, we need to install dependencies
by running the command below:
Next, let’s create the mocks. If the request is successful, a response with a status code
of 200 will be received.
mockApi.onPost('requests').reply(config) => {
if (config.data === validAuthentication) {
return [200, userDetails];
}
return [400, 'Incorrect username and password'];
});
codemagic.io 57
The mock is ready, so let’s write a test for an external API request:
Here, we’re testing a successful login with the correct credentials, using the native
JavaScript async await. Meanwhile, the authenticateUser function from Jest
authenticates the request and makes sure it matches our earlier snapshots. Similarly,
we test for an unsuccessful login in case incorrect credentials are used, such as an
incorrect email address or password.
Run yarn test or npm test in your terminal, and wait until the tests finish.
We have covered the basics of API testing for React Native applications using Jest.
With Jest, testing React Native applications has never been easier, especially with
snapshots, which ensure that the UI remains consistent regardless of the global styles.
The test-automation frameworks covered here are widely accepted and used for React
Native apps.
codemagic.io 58
Conclusion
With React Native, in most cases, you get the best of both worlds: You expend less
effort and take advantage of lower associated costs and speedy time to market while
still producing a robust and performant app. We hope this ebook provides you with a
foundation for your journey of testing React Native apps.
Writing exhaustive test cases and test suites for mobile apps requires a holistic
approach, practice and an inclusive mindset to think of all the edge cases or scenarios
to cover. Writing test cases and test suites adds value and trust amongst the
development team members and helps the team to deliver robust apps confidently. We
strongly believe that the concepts discussed in this ebook will be useful, whether
you’re testing individual components via a unit testing approach or an entire app via a
functional testing approach. Thank you for choosing the approach of building robust
apps!
codemagic.io 59
About the Authors
This book was published in cooperation between Codemagic and Sneh Pandya.
codemagic.io 60
Thank you for reading that book!
Build, test and deliver mobile apps in record time with Codemagic CI/CD!
codemagic.io
codemagic.io 61