As of this writing, there is no official testing guide for react-apollo. But there are a number of unofficial blog posts and articles. Here’s a sample:

So why write another blog post? Well, after a lot of spelunking, I came to the conclusion that most react-apollo developers approach tests in one of two ways:

  • They test their components with a mocked version of apollo
  • They separate apollo-related code from their components into containers, and only test the non-apollo components

After experimenting with both methods, I discovered a nice way to integrate these approaches. Others may have already done this, but I haven’t seen it explicitly articulated anywhere.

The key is to split tests into two types:

  • Tests that verify a component behaves correctly given certain props (component tests)
  • Tests that verify a container passes down the right props given certain mocked global state (container tests)

Sketching out the specs

Let’s try this on a real world example. Consider a ToDo application that allows a user to nest ToDos via a single input at the bottom of the screen. Here’s a gif of the behavior we want:

Gif

We’ll focus on the component at the bottom (the piece with the green background), which we’ll call <TaskCreator/>. We’ll keep all of the data queries and mutations in a separate container called <TaskCreatorContainer/>, and pass the data and mutations to <TaskCreator/> via props.

Now lets sketch out how we want our component and our container to behave via some unimplemented specs:

Component Specs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
describe('<TaskCreator />', () => {
context('when there is a parent task', () => {
it('displays the parent task name');
context('and the clear parent button is clicked', () => {
it('calls the onClickClearParent prop');
});
context('and a new task is entered', () => {
it('calls onClickCreate with a new task with the parent task id');
});
});
context('when there is no parent task', () => {
it('does not display the parent task section');
context('and a new task is entered', () => {
it('calls onClickCreate with a new task without a parent task id');
});
});
});

Source

Container Specs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('<TaskCreatorContainer />', () => {
context('when there is a parentId in the store', () => {
context('and the parentTask query will succeed', () => {
it('sets the parentTask prop to the query result');
context('and the onClickClearParent prop is called', () => {
it('dispatches the clearParentId action');
it('clears the parentTask prop');
});
});
});
context('when the createTask mutation will succeed', () => {
context('and the onClickCreate prop is called', () => {
it('calls the createTask mutation with the correct args');
});
});
});

Source

Implementing the specs

The first set of tests should be straighforward to anyone who’s tested react components, so we won’t go into them here. If you’re new to testing react components, I highly recommend getting comfortable with enzyme and implementing some regular, non-container tests before proceeding.

The second set of tests is trickier. The basic idea is to test that a component receives a certain set of props given a certain global state, which includes things like apollo query results, redux state, and react-router state.

This sounds sort of like an integration test that we could implement with selenium. But if we look at what containers actually are, we see that they’re just react components that query/mutate data. Selenium tests are better suited for final user flows once we want to test a bunch of containers and APIs interacting with each other. If we want to test the API of a single container, we’ll want to test a bunch of different inputs to that API, which would be difficult and slow to do via something like selenium.

In order to test how our container responds to different global state, we’ll need to mock it out. The <MockedProvider /> component inside of react-apollo is close to what we want, but we’ll need something that mocks out all of the global state relevant to our containers, not just apollo. Let’s call this hypothetical component <TestProvider/>. Here’s how we’d use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// We want to render a dummy component, not the real underlying component,
// because mounting the real component would be expensive. It also might render other
// containers that require other mocks. All we care about is that the container
// we're testing adds the correct props to whatever component it wraps.
const Component = () => null;
const Container = container(Component);
def('storeState', /* ...redux store state */);
def('graphqlMocks', /* ...some sort of graphql mocks*/);
def('container', () => mount(
<TestProvider storeState={$storeState} graphqlMocks={$graphqlMocks}>
<Container/>
</TestProvider>
));
subject('getProps', () => () => $container.update().find(Component).props());
describe('some prop', () => {
context('given some state', () => {
// ...modify $storeState or $graphqlMocks
it('gets set to something', () => {
expect($getProps().someProp).to.eql(something);
});
it('behaves some way when called', () => {
$getProps().someProp();
expect(somethingElse.called).to.be.true;
});
});
});

Source
NOTE: def is like rspec’s let, and sets the $ variables. Comes from bdd-lazy-var.

To mock out graphql responses for apollo 2.0, we’ll use apollo-schema-link. This allows us to hit the schema directly without any network requests. Then we will use addMockFunctionsToSchema from graphql-tools to mock out our resolvers. Both libraries are well documented and officially supported by apollo. Here’s how we’ll use them in <TestProvider/>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import React, { Component } from 'react';
import Faker from 'faker';
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { SchemaLink } from 'apollo-link-schema';
import { ApolloProvider } from 'react-apollo';
import PropTypes from 'prop-types';
// Import schema string instead of the schema executable so we don't
// end up importing server related code to the client
import schemaString from 'raw-loader!../output/schema';
// Helper in our codebase to create the apollo client
import { createClient } from 'lib/apollo_client';
export default class TestProvider extends Component {
static createSchema = () => makeExecutableSchema({ typeDefs: schemaString });
static propTypes = {
graphqlMocks: PropTypes.object,
}
constructor(props) {
super(props);
// Every instance should have it's own schema instance so tests
// don't bleed into one another
this.schema = TestProvider.createSchema();
this.apolloClient = createClient({ link: new SchemaLink({ schema: this.schema }) });
this.addDefaultMocks();
}
componentWillMount() {
const { graphqlMocks } = this.props;
this.mockGraphql(graphqlMocks);
}
componentWillReceiveProps({ graphqlMocks }) {
this.mockGraphql(graphqlMocks);
}
addDefaultMocks() {
addMockFunctionsToSchema({
schema: this.schema,
mocks: {
ID: () => Faker.random.uuid(),
String: () => Faker.lorem.sentence(),
}
});
}
mockGraphql(mocks = {}) {
addMockFunctionsToSchema({
schema: this.schema,
mocks,
});
}
render() {
const { children } = this.props;
return (
<ApolloProvider client={this.apolloClient}>
{children}
</ApolloProvider>
);
}
}

Source

Now let’s add some methods to mock out redux state and record actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import React, { Component } from 'react';
import Faker from 'faker';
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { SchemaLink } from 'apollo-link-schema';
import { Provider } from 'react-redux';
import { ApolloProvider } from 'react-apollo';
import PropTypes from 'prop-types';
import schemaString from 'raw-loader!../output/schema';
import { createClient } from 'lib/apollo_client'
// Helper in our codebase to create the redux store
import { store } from 'redux_utils';
export default class TestProvider extends Component {
static createSchema = () => makeExecutableSchema({ typeDefs: schemaString });
static createStore = (initialStoreState = {}) => store(initialStoreState, {
actionHistory: (state = [], action) => [action, ...state]
});
static propTypes = {
storeState: PropTypes.object,
graphqlMocks: PropTypes.object,
}
constructor(props) {
super(props);
this.schema = TestProvider.createSchema();
this.store = TestProvider.createStore();
this.apolloClient = createClient({ link: new SchemaLink({ schema: this.schema }) });
this.addDefaultMocks();
}
componentWillMount() {
const { graphqlMocks, storeState } = this.props;
this.setStoreState(storeState);
this.mockGraphql(graphqlMocks);
}
componentWillReceiveProps({ graphqlMocks, storeState }) {
this.setStoreState(storeState);
this.mockGraphql(graphqlMocks);
}
addDefaultMocks() {
addMockFunctionsToSchema({
schema: this.schema,
mocks: {
ID: () => Faker.random.uuid(),
String: () => Faker.lorem.sentence(),
}
});
}
mockGraphql(mocks = {}) {
addMockFunctionsToSchema({
schema: this.schema,
mocks,
});
}
setStoreState(storeState) {
this.store = TestProvider.createStore(storeState);
}
getStoreState() {
return this.store.getState();
}
getActions() {
return $testProvider.getStoreState().actionHistory;
}
getLastAction() {
return this.getActions()[0];
}
render() {
const { children } = this.props;
return (
<Provider store={this.store}>
<ApolloProvider client={this.apolloClient}>
{children}
</ApolloProvider>
</Provider>
);
}
}

Source

Depending on what you’re using for global state and what your containers look like, you might have some more methods in your <TestProvider/>. In our case, all we care about is redux and apollo, so we’re ready to use our <TestProvider/> in our container specs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import React from 'react';
import Faker from 'faker';
import { mount } from 'enzyme';
import { Factory } from 'rosie';
import { expect } from 'chai';
import waitUntil from 'wait-until-promise';
import _ from 'lodash';
import * as TaskActions from 'dux/tasks';
import TestProvider from 'test_provider';
import { container } from 'containers/task_creator';
const Component = () => null;
const Container = container(Component);
describe('<TaskCreatorContainer />', () => {
def('storeState', {});
def('graphqlMocks', {});
def('container', () => mount(
<TestProvider storeState={$storeState} graphqlMocks={$graphqlMocks}>
<Container/>
</TestProvider>
));
def('testProvider', () => $container.instance());
subject('getProps', () => () => $container.update().find(Component).props());
context('when there is a parentId in the store', () => {
def('parentId', () => Faker.random.uuid());
def('storeState', () => ({ tasks: { parentId: $parentId } }));
context('and the parentTask query will succeed', () => {
def('resolvedTask', () => Factory.build('task', { id: $parentId }));
def('taskQuery', () => sandbox.stub());
def('graphqlMocks', () => ({
Query: () => ({
task: $taskQuery.returns($resolvedTask),
})
}));
beforeEach(() => waitUntil(() => !$getProps().loading, 100, 10));
it('sets the parentTask prop to the query result', () => {
expect($getProps().parentTask).to.include(_.pick($resolvedTask, ['id', 'name']));
});
context('and the onClickClearParent prop is called', () => {
beforeEach(() => {
$getProps().onClickClearParent()
return waitUntil(() => !$getProps().loading, 100, 10);
});
it('dispatches the clearParentId action', () => {
expect($testProvider.getLastAction()).to.eql(TaskActions.clearParentId());
});
it('clears the parentTask prop', () => {
expect($getProps().parentTask).to.be.undefined;
});
});
});
});
context('when the createTask mutation will succeed', () => {
def('createTaskMutation', () => sandbox.stub());
def('createdTask', () => Factory.build('task', $onClickCreateArgs));
def('mutationResult', () => ({ task: $createdTask }));
def('graphqlMocks', () => ({
Mutation: () => ({
createTask: $createTaskMutation.returns($mutationResult),
})
}));
context('and the onClickCreate prop is called', () => {
def('onClickCreateArgs', { name: 'Blah' });
beforeEach(() => $getProps().onClickCreate($onClickCreateArgs));
it('calls the createTask mutation with the correct args', () => {
expect($createTaskMutation.args[0][1].input.task).to.eql($onClickCreateArgs);
});
});
});
});

Source

We now have implemented container specs. Hooray!

What we gained

So why’d we do all that?

We can now confidently refactor GraphQL queries/fragments

  • If we change a shared query or fragment, our tests ensure each dependent component is still getting the props it needs

We can now confidently refactor redux actions/reducers

  • If we change an action/reducer, our tests ensure that the props which are supposed to mutate redux are still having the intended effect

We can now confidently refactor shared hocs

  • If we change a shared hoc, our tests ensure each dependent component still gets the props it needs

We can now confidently refactor our schema

  • If we change the schema and make a query invalid, our tests will complain instead of giving us false positives

We don’t need to know so much about apollo anymore

  • If apollo changes their API (by changing/removing ‘loading’ from the data prop, for example), our tests will complain instead of giving us false positives
  • If apollo changes how it parses errors, we won’t need to change our tests, since we’re throwing the same errors we’re expecting to be thrown in the resolvers.

We now can hook our component up to other data sources without rewriting tests

  • If we want to use our component in some other context (maybe with a different query, or hydrated by something other than apollo), our component tests our still valid.

Feedback / Repo

Comments and suggestions appreciated!

If you’d like more details, check out the full repo for the ToDo example used throughout the article.