blog content
Definitive guide to a bug-less experience
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:
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 here. 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!
Step 2
Link RCTTest
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 linking here.
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""
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:
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:
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:
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:
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:
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:
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:
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.
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:
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!