Testing React Native + Apollo Apps
No hair-pulling necessary
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!
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!