Testing React Native + Apollo Apps

No hair-pulling necessary

Simon Tucker
React Native Training
6 min readSep 5, 2017

--

Writing tests for react-apollo can be tricky to set up, especially if you’re writing a React Native application. Let’s try and smooth out the process….

Setting up

I recommend using Jest as the test engine.

If you’re writing a React Native app, Jest should come preinstalled, but will require some extra configuration to work with react-apollo.

Jest is especially awesome when combined with IDE plugins like vscode-jest that autorun tests and show pass/fail inline in our code in real-time!

Are you kidding me?!

We need to install the babel-plugin-module-resolver to use react-apollo in our tests with ES6:

npm install --save-dev babel-plugin-module-resolver

We’ll configure Babel to alias react-apollo so it can be properly accessed:

{
"presets": ["react-native"],
"plugins": [
["module-resolver", {
"alias": {
"react-apollo": "./node_modules/react-apollo/react-apollo.browser.umd.js"
}
}]
]
}

Our package.json should look something like this:

// package.json
{
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest"
},
"dependencies": {
"apollo-client": "<current-version>",
"react-apollo": "<current-version>",
"react-native": "<current-version>",
},
"devDependencies": {
"babel-jest": "<current-version>",
"babel-plugin-module-resolver": "<current-version>",
"babel-preset-react-native": "<current-version>",
"graphql": "<current-version>",
"graphql-tools": "<current-version>",
"jest": "<current-version>",
"react-native-mock": "<current-version>",
"react-test-renderer": "<current-version>"
},
"jest": {
"testMatch": [
"**/__tests__/**/*.js",
"**/?(*.)(spec|test).js?(x)"
],
"preset": "react-native"
}
}

Writing Tests

A solid practice for testing react-apollo connected components is to test components and their GraphQL wrappers separately.

Let’s say we needed to test the following high-order component (HOC) Hero:

import {
ActivityIndicator,
StyleSheet,
FlatList,
Text,
View,
} from 'react-native';
import gql from 'graphql-tag';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import HeroItem from './hero-item.component';// get the user and all user's groups
export const HERO_QUERY = gql`
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
id
name
friends {
id
name
}
}
}
`;
const styles = StyleSheet.create({
container: {
alignItems: 'stretch',
backgroundColor: '#e5ddd5',
flex: 1,
flexDirection: 'column',
},
loading: {
justifyContent: 'center',
},
titleWrapper: {
alignItems: 'center',
position: 'absolute',
left: 0,
right: 0,
},
title: {
flexDirection: 'row',
alignItems: 'center',
},
titleImage: {
marginRight: 6,
width: 32,
height: 32,
borderRadius: 16,
},
});
class Hero extends Component {
constructor(props) {
super(props);
this.renderItem = this.renderItem.bind(this);
}
keyExtractor = item => item.id; renderItem = ({ item: friend }) => (
<HeroItem name={friend.name} />
)
render() {
const { loading, hero } = this.props;
// render loading placeholder while we fetch hero
if (loading || !hero) {
return (
<View style={[styles.loading, styles.container]}>
<ActivityIndicator />
</View>
);
}
// render list of friends for hero
return (
<View>
<Text>{hero.name}</Text>
<FlatList
ref={(ref) => { this.flatList = ref; }}
data={hero.friends}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ListEmptyComponent={<View />}
/>
</View>
);
}
}
Hero.propTypes = {
episode: PropTypes.string.isRequired,
hero: PropTypes.shape({
hero: PropTypes.string,
friends: PropTypes.array,
}),
loading: PropTypes.bool,
};
const heroQuery = graphql(HERO_QUERY, {
options: ownProps => ({
variables: {
episode: ownProps.episode,
},
}),
props: (results) => {
const { data: { loading, hero } } = results;
return { loading, hero };
},
});
export default heroQuery(Hero);

Our Hero component gets wrapped by the heroQuery via the react-apollo graphql module. When the HERO_QUERY executes, the wrapper passes on the loading and hero props to the Hero component, which rerenders based on its changed props.

Testing Components

The Hero component relies on the props it receives to determine how to render. We first simply need to test that our component renders the way we’d expect based on the received props. We can sneakily export the Hero component before it gets wrapped by our query so that we can test the Hero component in isolation:

// now we can --> import { Hero } from './hero.component'
export
class Hero extends Component {
...
}
...// we can still --> import Hero from './hero.component'
export default heroQuery(Hero);

Now we can use snapshot testing to make sure that our component renders as we expect:

// Hero Component testsimport 'react-native';
import React from 'react';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import { HERO_QUERY, Hero } from './hero.component';test('renders correctly', () => {
const loadingTree = renderer.create(
<Hero
episode={'JEDI'}
loading
/>,
).toJSON();
expect(loadingTree).toMatchSnapshot();
const tree = renderer.create(
<Hero
episode="JEDI"
hero={{
name: 'R2-D2',
friends: [
{
id: '1',
name: 'Luke Skywalker',
},
{
id: '2',
name: 'Han Solo',
},
{
id: '3',
name: 'Leia Organa',
},
],
}}
loading={false}
/>,
).toJSON();
expect(tree).toMatchSnapshot();
});

In a nutshell, snapshots compare a serializable result to a prior result — in our case, a JSONified version of the component — to see if anything changed since our last test. We’ll be notified if anything changes and can disregard it if we expected the change, or dig into the code if changes were unexpected. This keeps us from having to constantly update the expected output by hand. Check out this blog post for a deeper dive into the value of snapshot testing.

Testing React-Apollo Queries

Now that we know the Hero component will render reliably when passed props, we need to make sure our graphql wrapper will execute the correct GraphQL query and pass the expected props to the Hero component.

We don’t need to use the Hero component to test our heroQuery. We don’t really want to use the Hero component either since it’s better to keep components and queries decoupled, especially during testing.

We can use the same trick and export heroQuery like we did with the Hero component:

export const heroQuery = graphql(HERO_QUERY, { ... });

For our test, we can use the MockedProvider class supplied by react-apollo/test-utils. MockedProvider accepts a mocks prop where we supply an array of GraphQL request/result pairs. Each request contains a GraphQL query and variables, and the associated results are the data to be returned if that exact query + variables gets executed from a child component within MockedProvider.

We can wrap a DummyComponent with our heroQuery and stick it inside MockedProvider. We’ll pass the query that should get executed by heroQuery to MockedProvider, and then test that the received props in the DummyComponent match what we would expect for the given query:

// heroQuery testsimport 'react-native';
import React from 'react';
import { addTypenameToDocument } from 'apollo-client';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
// Note: We need to access this file directly from node_modules or things break
import { MockedProvider } from '../../node_modules/react-apollo/test-utils';
import { HERO_QUERY, heroQuery } from './hero.component';test('heroQuery correctly delivers props to child component', (done) => {
const variables = { episode: "JEDI" };
const mockedData = {
"hero": {
"__typename": "Droid",
"id": "2001",
"name": "R2-D2",
"friends": [
{
"__typename": "Human",
"id": "1000",
"name": "Luke Skywalker"
},
{
"__typename": "Human",
"id": "1002",
"name": "Han Solo"
},
{
"__typename": "Human",
"id": "1003",
"name": "Leia Organa"
}
]
}
};
// use this component to make sure the right props are returned
class DummyComponent extends React.Component {
componentWillReceiveProps({ loading, hero }) {
if (!loading) {
expect(hero).toEqual(mockedData.hero);
expect(hero).toMatchSnapshot();
done();
} else {
expect(loading).toBe(true);
}
}
render() {
// doesn't need to actually render anything
return null;
}
}
// wrap the dummy with our query
const WrappedDummyComponent = heroQuery(DummyComponent);
// apollo-client includes __typename in queries/results by default
// so we need to make sure our test query looks that way as well
const query = addTypenameToDocument(HERO_QUERY);
const mock = (
<MockedProvider mocks={[
{
request: { query, variables },
result: { data: mockedData },
},
]}>
<WrappedDummyComponent
episode="JEDI"
/>
</MockedProvider>
);
renderer.create(mock);
});

That’s it! Our component renders the way it’s supposed to, and our query executes the way it’s supposed to and returns the right props!

Hopefully this gives you a good jumping-off point for writing tests for your React Native apps with Apollo. From this foundation, we can build more complicated tests for things like query side-effects and other fun stuff.

As always, please share your thoughts, questions, struggles, and breakthroughs below!

--

--