Unit Testing React Native Components: A Firsthand Guide
This is the first post in a series that serves as an expanded edition of a talk I gave at a meetup with the NYC JavaScript React Group.
Update (Aug ‘16): A section of this post is about making the unit test runner Jest play nicely with React Native. The Jest team’s contributors have put a tremendous amount into the package over the last few months, and it now integrates seamlessly with React Native out of the box! Here is an informative GitHub comment about the recent history of Jest: https://github.com/facebookincubator/create-react-app/pull/250#issuecomment-237098619
—
As an engineer at Refinery29 who mostly writes code for the web, I was very excited to help write our first iOS app — This AM — using React Native. The prospect of building a native mobile app mostly in JavaScript seemed like an opportunity worth taking.
For more on why we chose React Native, I recommend reading this post from Benjamin Wilson, our mobile tech lead. In short, we thought This AM was a great candidate for this new approach as it is designed to be very minimalist. (As far as what the app actually does — it’s a simple and beautiful daily rundown of the news you need to know for the day.)
As any developer knows, there are pros and cons when working with a brand new technology. On one hand, React Native definitely fulfills its central promise of enabling any front end developer to build for native platforms. On the other hand, new versions of React release every two weeks, and many libraries and standards are in flux, slowing down work that could have been already established as boilerplate. My colleague Ilana Sufrin has blogged about a number of specific challenges we faced with React Native here.
Our Testing Stack
One of our goals was to follow best practices in terms of automated testing, since we believe that it’s critical to good development. We wanted to have comprehensive unit tests that could give us the confidence to refactor our code, truly end-to-end feature/integration tests that touched every part of the entire application, and a stable continuous integration environment we could rely on to run these tests for us.
Since React Native is still new, there wasn’t much of a consensus as far as best practices in these areas. We experimented with a few different combinations of testing libraries, keeping in mind that we were covering new ground and that support and feedback systems would be thin. We eventually settled on a setup that worked for our day-to-day process:
- Jest for unit tests
- Calabash and Cucumber for feature/integration tests
- Running both unit tests and feature/integration tests in full on Travis
I will be focusing on how we wrote our unit tests for now, and will be writing more about our feature/integration tests and integrating with Travis in upcoming posts.
Unit testing components
Writing unit tests for a React Native app using the recommended test library, Jest, is similar to doing the same for a React web app: you write simple and dumb components, and test the output of their render methods using shallow rendering. We might test that we render another component as a child of our primary component:
export default function Foo(props) {
return <Bar />;
}describe('Foo component', function() {
it('renders a TextCard component', function() {
const renderer = TestUtils.createRenderer();
renderer.render(<Foo />);
const result = renderer.getRenderOutput();
expect(result.props.children.type).toEqual(Bar);
});
});
Using Jest with React Native
As noted in the update above, this section is out of date, since Jest now works better with React Native. I’ve retained the section for historical purposes.
Jest’s primary distinguishing feature is that for every function tested, all imported modules (whether from node_modules or from another part of your app) are mocked out by default. Jest accomplishes this by traversing your dependency tree and creating type-appropriate mocks for every require/import statement. A function you pull in will be mocked out as a function that does not return anything. An object you import will be mocked out as an empty object. And so on.
Jest’s auto-mocking behavior works well for the majority of the npm modules we were using in This AM. But one roadblock we ran into with the version of Jest that we were using at the time was that Jest could not properly mock out the react-native module, by virtue of that package utilizing various constants that are typically declared by the iOS side of the framework (which, of course, doesn’t exist at all when we’re running Node-powered unit tests). Jest would fail in its traversal step because of these constants.
A popular solution advocated to solve this is quite clever: simply use the non-native react library as your mock for react-native.
// components/__mocks__/react-native.js
import React from ‘react’;
export default Object.assign({}, React);
While this setup allows us to render our custom components, it doesn’t allow us to easily test that we’ve rendered sub-components that exist in react-native but not in react, like View or ActivityIndicatorIOS. In order to do that, we have to mock them all out as React component classes:
// components/__mocks__/react-native.jslet { assign, each } = require.requireActual('lodash');
let React = require.requireActual('react')function createMockComponent(componentName) {
return React.createClass({
render: function() {
return React.createElement(componentName, this.props);
}
});
};let rnComponents = {};
each([
'ActivityIndicatorIOS',
// other React Native iOS-specific components like Image,
// View, Text, ScrollView, etc…
], (componentName) => {
rnComponents[componentName] = createMockComponent(componentName);
});export default Object.assign({}, React, rnComponents);
Then, for our unit tests:
export default function Foo(props) {
return (
<ActivityIndicatorIOS />
);
}describe('Foo component', function() {
it('renders a ActivityIndicatorIOS component', function() {
const renderer = TestUtils.createRenderer();
renderer.render(<Foo />);
const result = renderer.getRenderOutput();
expect(result.props.children.type)
.toEqual(ActivityIndicatorIOS);
});
});
We’re only making an assertion about the type here — but since the mocked ActivityIndicatorIOS is an actual React component, we can also look at any props that Foo might pass into it.
Writing better tests using react-shallow-testutils
The unit test examples above cover simple use cases — they’re only comparing the types of the rendered sub-components. We might also want to test props that are passed in, or count the number of components of a particular type or class. For purposes like this, we have found the react-shallow-testutils package to be very helpful. Combined with a utility library like lodash, we can write short yet comprehensive tests:
import { findAllWithType } from 'react-shallow-testutils';
import map from 'lodash/map';export default function Foo(props) {
return (
<View>
<Image source=”/image1.jpg”>
<Image source=”/image2.jpg”>
<Image source=”/image3.jpg”>
</View>
);
}describe('Foo component', function() {
it('renders 3 Image components with the right props', function() {
const renderer = TestUtils.createRenderer();
renderer.render(<Foo />);
const result = renderer.getRenderOutput();
const sources = map(findAllWithType(result, Image),
'props.source');
expect(sources).toDeepEqual([
'/image1.jpg', '/image2.jpg', '/image3.jpg'
]);
});
});
Feedback
We welcome anyone to leave notes with suggestions of any kind — we’d love to hear what’s worked for you, and if you’ve found better approaches to cleanly writing these tests. You can also send an email to thisam@refinery29.com. I’ll be back soon with our learnings with regards to feature/integration tests, so stay tuned!