Testing Your React Native Apps

React.js is very straightforward due to the fact that the browser is the only environment we need to worry about. With React Native, however, the ecosystem is significantly more complex because it involves multiple platforms and testing suites for web, iOS, Android, and possibly even Windows soon.

In this series of posts, we will cover each platform and its tooling separately. Today we will start simply with iOS, which is the easiest and shortest to set up. Let’s check how snapshots can help us iterate fast and make our React Native upgrade process a breeze.

Snapshots

Screenshot testing is great because it helps mediate the differences in all platforms and devices your app can be running. The process itself is rather simple: reference images stored on the disk are compared with the current state of the app. In the event they differ, the suite finishes with an error.

Some time ago React Native released an update which introduced a subtle, yet important UI change. The update caused all <ListView />nodes to render with slightly different margins, which ended up breaking a lot of layouts. Having a convenient way to check our UI at a glance would’ve saved a lot of headache for many people. Fortunately, this type of problem is no longer an issue.

Take 10 minutes out of your day and let me show you how to save hours down the road! All that’s required is 1/2 cup of coffee.

Prerequisites

Empty app

I have slightly modified the default React Native Hello World example. Instead of rendering dummy text, it now renders an array of strings below the heading.

The folder structure is as follows:

app
/components
List.js
/containers
App.js
index.ios.js
index.test.js
package.json

There are two index files at the root of the folder:

  • index.ios.js — default entry point for our app where we register SampleApp by calling AppRegistry.registerComponent,
  • index.test.js — entry point for our integration tests that registers all componentsand containers. We will discuss this file later in this guide.

You can browse it live on GitHub. It contains a working example of what we’ll be building today. Feel free to set it up on your Mac first and check the steps as we go!

Step 1: Let’s make some space

Before we start writing our tests, we’ll have to create a new target for them.

To do so, go to File -> New -> Target and select iOS Unit Testing Bundle from Test section.

You can create as many targets as you want to organise your tests by their type, as in UIExplorer

Now define a name for your target.

In this guide, we are going to go with SampleAppSnapshotTests.

Your tests can be also written in Swift, but in this guide we are going to stick with Obj-C for consistency. Once you are happy with the product name, click “Finish” and let’s go to next step!

On iOS we are going to use RCTTest most of the time. It’s aReact Native module that contains helpers for making snapshot tests and more. We will discover them in next guides later. For now, let’s just linkit so that we can start using it.

Note: :rnpm currently does not support linking to non-main target. That means you will need to follow the manual linking guide as described below. If any of these steps sound unfamiliar to you, be sure to check the official guide on manual linking.

First thing we’ll need to add RCTTest.xcodeproj to our Libraries group.

Next, we’ll add libRCTTest.a under Link binary with libraries section. You can find it in Build Phases. Note we are adding it to the test target so that our tests and its dependencies do not ship to the App Store.

Next, update Header Search Paths of our SampleAppSnapshotTests target. Note the path is `recursive`.

Last but one step is to add two linker flags (ObjC and lc++) to our target

And finally, time to specify FB_REFERENCE_IMAGE_DIR. It holds a path to the place on disk where our suite will store captured snapshots.

You can use the below value:

FB_REFERENCE_IMAGE_DIR=""$(SOURCE_ROOT)/$(TARGET_NAME)/ReferenceImages""

FB_REFERENCE_IMAGE_DIR=""$(SOURCE_ROOT)/$(TARGET_NAME)/ReferenceImages""

which will expand to /path/to/project/ios/SampleAppSnapshotTests/ReferenceImages

and add it to Preprocessor Macros.

Now, we can finally go back to SampleAppSnapshotTests.m. It’s a default test file that Xcode creates every time we make a new test target.

Step 3: Setting up the runner

After opening up the test file for the first time, you should see the following:

//
// SimpleAppSnapshotTests.m
// SimpleAppSnapshotTests
//
// Created by Michał Grabowski on 30/04/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <XCTest/XCTest.h>
@interface SimpleAppSnapshotTests : XCTestCase
@end
@implementation SimpleAppSnapshotTests
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
@end

This is a default test case created by Xcode. Since it’s been set up for iOS tests in mind, we can delete all methods except setUp, which we will use in a bit.

We are going to extend our SampleAppSnapshotTests interface with a private property _runner:


#import
#import
#import
@interface SampleAppSnapshotTests : XCTestCase {
 RCTTestRunner *_runner;
}

This will hold a reference to RCTTestRunner — which we will use to run our components and make snapshots of them.

Next, we’ll replace our setUp method with the following:

- (void)setUp {
_runner = RCTInitRunnerForApp(@"index.test", nil);
_runner.recordMode = NO;
}

The first argument to RCTInitRunnerForAppis a location where our entry test file lives. In this case — it’s in the root folder so we can just put its name.

On the second line, there’s recordMode defined. When set to NO, it will run all tests in order to compare the snapshot with the reference image which is stored on disk. When set to YES, it will capture snapshots of every test case and store them on disk for further use.

Step 4: Writing our test case

Before we start with our first native test case, let’s take a look at our index.test.js file:

const React = require('react');
const { AppRegistry, SnapshotViewIOS, NativeModules } = require('react-native');
/**
* Require here every single component you would like
* to test
*/
const TO_TEST = [
// require('./app/components/List'), maybe later ;)
require('./app/containers/App'),
];
/**
* Register every test component
*/
TO_TEST.forEach(Component => {
/**
* We have to wrap every component in a `SnapshotViewIOS`
* so that our test suite knows when the screen has been captured
* and suite is finished.
*
* We also pass `initialProps` down so we can manipulate the
* component apperance from the test suite
*/
const Snapshotter = (props) => (
<SnapshotViewIOS>
<Component {...props} />
</SnapshotViewIOS>
);
/**
* Register every component by its displayName we will refer from the test
* suite
*/
AppRegistry.registerComponent(Component.displayName, () => Snapshotter);
});
view raw index.test.js hosted with ❤ by GitHub

It has an array of components to test (called TO_TEST). We pass each item to AppRegistry.registerComponent. That means we can test any component present in this array directly.

Note: Before we pass a component to AppRegistry.registerComponent, we wrap it with the SnapshotViewIOS. This is responsible for capturing underlying views and finishing the test case. Without it, our tests would hang forever.

We also pass initialProps on line 28. That means we change the way our components display on per test basis. We will explore this pattern later.

Let’s open SampleAppSnapshotTest.m again and add the below code under setUp method:

- (void)testAppContainer {
[_runner runTest:_cmd module:@”AppContainer”];
}

It’s our first test case! We are using RCTTestRunner to run a test with the AppContainer module.

Note: Module name (in this case AppContainer) is the string you pass to AppRegistry.registerComponent. In our case, we register containers and components by their displayName(line 36 in index.test.js).

After performing that step, your entire file should look more or less like the one below:

#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <RCTTest/RCTTestRunner.h>
@interface SampleAppSnapshotTests : XCTestCase {
RCTTestRunner *_runner;
}
@end
@implementation SampleAppSnapshotTests
- (void)setUp {
_runner = RCTInitRunnerForApp(@"index.test", nil);
_runner.recordMode = NO;
}
- (void)testAppContainer {
[_runner runTest:_cmd module:@"AppContainer"];
}
@end

Step 5: Running tests

Did you notice the small icon that appeared next to your test case?

It’s a small helper from Xcode that allows us to run a single test suite when clicked.

So let’s try to run our example by clicking the icon and see what happens!

The test most likely failed straight away! This is because there’s no image to compare against.

Step 6: Recording snapshots

As we’ve seen already in the previous step, to actually run the test, we have to snapshot it first!

Let’s change the recordMode from NO to YES so that it looks as follows:

- (void)setUp {
_runner = RCTInitRunnerForApp(@”index.test”, nil);
_runner.recordMode = YES;
}

and run our suite again!

This time, our tests pass and there’s a new snapshot in ReferenceImages.

Now, we can turn off the recordMode by setting its value back to NO and run the suite again.

Woo! Tests are green — the snapshot matches the current state of our app!

Tip: You can now edit app/containers/App.js and change e.g. backgroundColor. It will make our test fail as that’s rather a significant change! Don’t forget to revert the changes before continuing onto next step!

Bonus A

Adjusting the snapshot size

As you‘ve noticed, the snapshot height is 2000 points long! This is the default size so that UIExplorer examples can fit in a single view.

We’ve aligned our view with flexbox to take all available space. That means the default snapshot dimensions don’t reflect real users phones. Time to change that!

Let’s turn on the recordMode back to YES and add the following test case:

// add to the top
#import "RCTRootView.h"
- (void)testAppContainerSmall {
[_runner runTest:_cmd module:@”AppContainer” initialProps:@{}
configurationBlock:^(RCTRootView *view) {
view.frame = CGRectMake(0, 0, 320, 568);
}];
}

We are going to tell change RootView frame to be 320 x 568 (iPhone 6). Let’s run the tests again.

We should now have a new file inside our ReferenceImages folder.

That looks way better, doesn’t it?

Note: You can use this technique to snapshot different screen sizes. In this guide, we run all our tests on iPhone 5. As you can see, that didn’t stop us from capturing iPhone 6 snapshot.

Bonus B

Mocking the data

Up to this point we’ve only snapshotted the views as is. Our view accepts data as a prop. When we pass it an empty array, it renders an empty placeholder.

class App extends React.Component {
static displayName = 'AppContainer';
static propTypes = {
data: React.PropTypes.array,
};
static defaultProps = {
data: ['Item #1', 'Item #2', 'Item #3', 'Item #4'],
};
render() {
const heading = this.props.data.length > 0
? 'Welcome to React Native!'
: 'No items to render';
return (
<View style={styles.container}>
<Text style={styles.welcome}>
{heading}
</Text>
<List data={this.props.data} />
</View>
);
}
}
view raw AppContainer.js hosted with ❤ by GitHub

Thanks to our index.test.js setup, we can pass arbitrary initialProps to the tested component. That allows us to manipulate its appearance on a per test basis. To do so, we will define yet another test case that passes an empty array as follows:

- (void)testAppContainerSmallWithoutData {
[_runner runTest:_cmd module:@”AppContainer”
initialProps:@{@”data”: @[]}
configurationBlock:^(RCTRootView *view) {
view.frame = CGRectMake(0, 0, 320, 568);
}];
}

Let’s turn on the recording mode again, run the tests.

Woo again! We’re also now able to snapshot an empty view. Our test coverage is increasing.

Summary

I hope you enjoyed this short article, and if I have saved even a single person several hours of frustration with configuring testing, then I have done my job! Look for more articles in the near future for the other platforms.

You can find the working example on Github. Check it out and let me know what you think in comments.

Good luck and have fun!

Insights

Learn more about Testing

How AI Agents Can Cut Manual QA by 30% — cover

Oct 30Article

How AI Agents Can Cut Manual QA by 30%

Explore an agentic QA workflow targeting 30% less manual effort across mobile, web, and TV, with test selection, failure triage, and validation.

Give QA Agents the Controls They Need With Appduct — cover

Oct 20Article

Give QA Agents the Controls They Need With Appduct

Use Appduct to give AI QA agents access to app functions for setting test conditions, triggering events, and checking results with UI automation.

cover

Oct 13Article

Agentic Mobile Testing With e2e and agent-device

See how e2e uses agent-device for AI-driven iOS and Android tests, combines exact assertions with agent actions, and replays cached steps without model calls.

Who Tests AI-Written Code? Inside TesterArmy With Szymon Rybczak — cover

Oct 12Podcast

Who Tests AI-Written Code? Inside TesterArmy With Szymon Rybczak

Szymon Rybczak explains how TesterArmy tests web and mobile apps with AI agents, creates test plans for pull requests, and fits into QA workflows.

cover

Aug 4Article

Automating Mobile QA With Cloud Agents

Code review and green CI checks do not prove a mobile app still works. Learn how cloud agents, agent-device, and runtime evidence can verify app behavior before merge.

cover

Jul 21Webinar

Behavior Review for AI-Native Delivery

AI can increase code output faster than teams can verify the real app. See why behavior review matters before merge.

cover

Jul 6Article

Introducing Sniffler: Run only the E2E tests your changes can affect

Sniffler maps changed files to affected E2E tests, so teams can avoid running the full suite on every pull request.

cover

Jun 17Talk

Automating Mobile QA with Cloud Agents

Michał Pierzchała will present Automating Mobile QA with Cloud Agents at Ship London on 17 June 2026.

cover

May 13Podcast

How AI Is Changing React Native Development and Testing

Zack Ebenfeld joins React Universe On Air to talk about how AI is changing React Native workflows, testing, QA, and day-to-day engineering work.

cover

Apr 14Article

Agent React DevTools: Give AI Agents Access to React Internals

Agent React DevTools gives AI agents access to the React component tree, state, and performance data, enabling deeper debugging and more reliable QA for React and React Native apps.

cover

Mar 19Live Stream

Agent Device, AI workflows, and mobile testing in practice

A discussion on Agent Device, AI-driven workflows, and testing mobile apps using simulators and accessibility data.

cover

Aug 23Podcast

Introducing E2E Tests to React Native Core

This episode of the React Native Show dives into the recent introduction of End-to-End (E2E) testing in RNTester. Join us as we unpack the thought process, technical considerations, and practical implementation of this new approach to testing in React Native.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.