This article is written by Sneh Pandya and focuses on React Native testing
Mobile apps play a crucial role in people’s lives. The bar is set high for all mobile apps in the market. Mobile app users have naturally grown demanding. Plenty of alternatives are available to download and use, so users do not tolerate bugs in any app. They are also not shy about sharing their love or hate for any mobile app these days – and that feedback surfaces in front of millions of other users in seconds.
In this article, we’ll look at popular and available options for testing React Native apps. All mobile apps are built using composition. Smaller components are brought together to form higher-level components and achieve greater functionality. The best testing practice is to run tests that cover functionality across the different levels of composition.
The primary focus can be on the highest level, functional testing, but React Native apps can be tested and testing can be automated on multiple layers, including:
Unit testing for React Native apps
Integration testing for React Native apps
Component testing for React Native apps
Functional testing for React Native apps
React Native 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. A variety of functional test-automation frameworks are available for React Native testing.
While unit testing can be performed at the component level, functional tests provide better capabilities for testing the larger entities in React Native apps. Unit testing for a particular component’s logic can be done in isolation by using traditional JavaScript libraries and making React Native return bare components instead of native ones. With functional test-automation frameworks, UI components can be easily tested as a whole.
The best thing about React Native apps is that they are fully compatible with native components in both Android and iOS ecosystems. This ensures compatibility with more frameworks, tools and native methods for testing. Let’s start by writing tests in JavaScript to perform React Native unit testing using Jest and Jasmine.
Unit testing for React Native
By default, React Native provides Jest tests for unit testing, and this 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: title description and 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();
});
it("should be able to play a song", function() {
music.play(song);
expect(music.currentlyPlayingSong).toEqual(song);
// demonstrates use of custom matcher
expect(music).toBePlaying(song);
});
describe("when song is paused", function() {
beforeEach(function() {
music.play(song);
music.pause();
});
it("should indicate the song is paused", function() {
expect(music.isPlaying).toBeFalsy();
// demonstrates use of 'not' with a custom matcher
expect(music).not.toBePlaying(song);
});
it("should be possible to resume", function() {
music.resume();
expect(music.isPlaying).toBeTruthy();
expect(music.currentlyPlayingSong).toEqual(song);
});
});
// demonstrates use of spies to intercept and test method calls
it("shows the current song whether the user has made it a favorite", function() {
spyOn(song, 'persistFavoriteStatus');
music.play(song);
music.makeFavorite();
expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
});
// demonstrates use of expected exceptions
describe("#resume", function() {
it("should throw an exception if song is already being played", function() {
music.play(song);
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 communication between them via a bridge.
Integration testing for React Native
While performing React Native testing via integration tests, the communication goes through the bridge and requires both native components 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
generated functions to provide the ability to refer 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:
yarn add cavy --dev
or npm:
npm i --save-dev cavy
For your app, you need to import Tester
, TestHookStore
and the specs in a top-level JavaScript file, as shown below:
import React, { Component } from 'react';
import { Tester, TestHookStore } from 'cavy';
import AppSpec from './specs/AppSpec';
import App from './app';
const testHookStore = new TestHookStore();
export default class AppWrapper extends Component {
render() {
return (
<Tester specs={[AppSpec]} store={testHookStore} waitTime={4000}>
<App />
</Tester>
);
}
}
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.
import React, { Component } from 'react';
import { TextInput } from 'react-native';
import { hook } from 'cavy';
class Scene extends Component {
render() {
return (
<View>
<TextInput
ref={this.props.generateTestHook('Scene.TextInput')}
onChangeText={...}
/>
</View>
);
}
}
const TestableScene = hook(Scene);
export default TestableScene;
Now, we can write the spec functions referencing the already hooked components.
export default function(spec) {
spec.describe('Feature', function() {
spec.it('works', async function() {
await spec.fillIn('Scene.TextInput', 'random string')
await spec.press('Scene.button');
await spec.exists('NextScene');
});
});
}
Then register the AppWrapper as the main entry point with AppRegistry instead of the current App component, as shown below:
AppRegistry.registerComponent('AppWrapper', () => AppWrapper);
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)
// Presses the identified component. The component must respond to onPress.
press(identifier)
// 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)
// Returns true if the component can be identified or is found on the screen
exists(identifier)
// Returns true if the component cannot be identified or is not found on the screen
notExists(identifier)
// Returns the identified component. Can be used if the component doesn't respond to either onChangeText or onPress.
findComponent(identifier)
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
.
Functional testing for React Native
Multiple open-source test-automation frameworks are available for streamlining the app development process and maximizing testing coverage. The best choice is a framework that can support multiple platforms and provide a robust foundation for test automation.
A range of platform-specific frameworks are also available to choose from. Naturally, each framework is built for a particular platform, and in most cases, it is easier to adopt for a specific platform.
Let’s take a look at the simple example below:
<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 what different options are available for test automation and what programming languages can be used for testing.
React Native testing using Appium
Appium is an open-source test-automation framework for native, hybrid and mobile web apps. It uses Selenium WebDriver internally. Because of this, Appium can work extremely well for the mobile web as well, and the use cases are similar if Selenium is used for web-based testing.
Appium and its scripts can work exactly the same on both platforms. In addition, Appium provides fantastic programming language support. Developers can write tests using their favorite language (like Java, Ruby, Python or C#), tools and preferred environment. It’s easy to get started, create reusable tests, and execute tests on real devices.
When it comes to React Native-powered apps, JavaScript isn’t necessarily required. Appium scripts look like the following:
driver.findElement(By.id("com.example.app:id/radio0")).click();
driver.findElement(By.id("com.example.app:id/radio1")).click();
driver.findElement(By.id("com.example.app:id/radio2")).click();
driver.findElement(By.id("com.example.app:id/editText1")).click();
driver.findElement(By.id("com.example.app:id/editText1")).sendKeys("Simple Test");
driver.findElement(By.name("Answer")).click();
// or alternatively, like this:
driver.findElement(By.id("com.example.app:id/button1")).click();
On iOS, Selenium WebDriver gets a command from an Appium script and sends the command in the form of JSON via an HTTP request to the Appium server. On Android, things work almost the same way, except the frameworks used are Selendroid and UiAutomator.
Learn more about how Appium can be used for React Native testing.
React Native testing using Calabash
Calabash enables anyone to write tests for mobile applications. Calabash tests are written in Gherkin and run in Cucumber. All tests are simple and easy to read yet executable by the automation system. It is easier to write test cases because of the straightforward vocabulary and specification-oriented language. An example is shown below:
Feature: Answer the questions feature
Scenario: As a valid user, I want to answer the question,
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
.
React Native testing using Espresso for Android
Espresso provides APIs for writing UI tests to simulate user interactions for Android apps. The Espress API is lightweight and provides three main components: viewMatchers
, viewActions
and viewAssertions
. Espresso provides automatic synchronization of test methods and UI elements that are being tested. This makes test execution extremely fast because no test scripts need to include any sleep or wait commands. An example is shown below:
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.
React Native testing using XCTEST for iOS
[XCTest] comes with Xcode but is also usable 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 proves to trivial because it is fully compatible with both Objective-C and Swift.
Here’s how UI components would look with Objective-C:
- (void)testClicksOnRadioButtons {
[tester tapViewWithAccessibilityLabel:@”Radio1”];
[tester tapViewWithAccessibilityLabel:@”Radio2”];
[tester tapViewWithAccessibilityLabel:@”Radio3”];
[tester enterText:@”Simple Test”
intoViewWithAccessibilityLabel:@”editText1”];
[tester tapViewWithAccessibilityLabel:@”Answer”];
}
Alternatively, with Swift, the test would look as simple as this:
testClicksOnRadioButtons() {
let app = XCUIApplication()
app.radiobutton[0].tap()
app.radiobutton[1].tap()
app.radiobutton[2].tap()
app.staticTexts[“Simple Test”]
app.button[0].tap()
}
Conclusion
We have covered the basics of React Native testing with various approaches and how the apps can be tested using various frameworks. The test-automation frameworks covered here are widely accepted and used for native mobile apps, hybrid apps, the mobile web and React Native apps. The programming language used to build mobile apps is not critical because it won’t have any influence on the test-automation frameworks.
Useful links and resources
Here’s an article on why Android developers should pay attention to React Native in 2021.
Here’s an article on running React Native Detox tests on Codemagic.
Here’s an article on testing React Native apps with Jest and Codemagic.
Here’s an article on choosing the right database for your React Native app.
Here’s an article on how to improve performance of a React Native app.
Here’s a practical guide on Continuous Integration and Delivery for React Native apps.
For discussions, learning and support, join the Codemagic Slack Community.
Sneh is a Senior Product Manager based in Baroda. He is a community organizer at Google Developers Group and co-host of NinjaTalks podcast. His passion for building meaningful products inspires him to write blogs, speak at conferences and mentor different talents. You can reach out to him over Twitter (@SnehPandya18) or via email (sneh.pandya1@gmail.com).