Testing React components
Have peace of mind when using React Apollo in production
Running tests against code meant for production has long been a best practice. It provides additional security for the code that's already written, and prevents accidental regressions in the future. Components utilizing React Apollo, the React implementation of Apollo Client, are no exception.
Although React Apollo has a lot going on under the hood, the library provides multiple tools for testing that simplify those abstractions, and allows complete focus on the component logic. These testing utilities have long been used to test the React Apollo library itself, so they will be supported long-term.
An introduction
The React Apollo library relies on React's context to pass the ApolloClient
instance through the React component tree. In addition, React Apollo makes network requests in order to fetch data. This behavior affects how tests should be written for components that use React Apollo.
This guide will explain step-by-step how to test React Apollo code. The following examples use the Jest testing framework, but most concepts should be reusable with other libraries. These examples aim to use as simple of a toolset as possible, so React's test renderer will be used in place of React-specific tools like Enzyme and react-testing-library.
Note: As of React Apollo 3, all testing utilities can now be found in their own
@apollo/react-testing
package.
Consider the component below, which makes a basic query, and displays its results:
1import React from 'react';
2import gql from 'graphql-tag';
3import { useQuery } from '@apollo/react-hooks';
4
5// Make sure the query is also exported -- not just the component
6export const GET_DOG_QUERY = gql`
7 query getDog($name: String) {
8 dog(name: $name) {
9 id
10 name
11 breed
12 }
13 }
14`;
15
16export function Dog({ name }) {
17 const { loading, error, data } = useQuery(
18 GET_DOG_QUERY,
19 { variables: { name } }
20 );
21 if (loading) return <p>Loading...</p>;
22 if (error) return <p>Error!</p>;
23
24 return (
25 <p>
26 {data.dog.name} is a {data.dog.breed}
27 </p>
28 );
29}
1import React from 'react';
2import gql from 'graphql-tag';
3import { Query } from 'react-apollo';
4
5// Make sure the query is also exported -- not just the component
6export const GET_DOG_QUERY = gql`
7 query getDog($name: String) {
8 dog(name: $name) {
9 id
10 name
11 breed
12 }
13 }
14`;
15
16export const Dog = ({ name }) => (
17 <Query query={GET_DOG_QUERY} variables={{ name }}>
18 {({ loading, error, data }) => {
19 if (loading) return <p>Loading...</p>;
20 if (error) return <p>Error!</p>;
21
22 return (
23 <p>
24 {data.dog.name} is a {data.dog.breed}
25 </p>
26 );
27 }}
28 </Query>
29);
Given this component, let's try to render it inside a test, just to make sure there are no render errors:
1// Broken because it's missing Apollo Client in the context
2it('should render without error', () => {
3 renderer.create(<Dog name="Buck" />);
4});
This test would produce an error because Apollo Client isn't available on the context for the useQuery
Hook to consume.
In order to fix this we could wrap the component in an ApolloProvider
and pass an instance of Apollo Client to the client
prop. However, this will cause the tests to run against an actual backend which makes the tests very unpredictable for the following reasons:
The server could be down.
There may be no network connection.
The results are not guaranteed to be the same for every query.
1// Not predictable
2it('renders without error', () => {
3 renderer.create(
4 <ApolloProvider client={client}>
5 <Dog name="Buck" />
6 </ApolloProvider>,
7 );
8});
MockedProvider
The @apollo/react-testing
package exports a MockedProvider
component which simplifies the testing of React components by mocking calls to the GraphQL endpoint. This allows the tests to be run in isolation and provides consistent results on every run by removing the dependence on remote data.
By using this MockedProvider
component, it's possible to specify the exact results that should be returned for a certain query using the mocks
prop.
Here's an example of a test for the above Dog
component using MockedProvider
, which shows how to define the mocked response for GET_DOG_QUERY
:
1// dog.test.js
2
3import { MockedProvider } from '@apollo/react-testing';
4
5// The component AND the query need to be exported
6import { GET_DOG_QUERY, Dog } from './dog';
7
8const mocks = [
9 {
10 request: {
11 query: GET_DOG_QUERY,
12 variables: {
13 name: 'Buck',
14 },
15 },
16 result: {
17 data: {
18 dog: { id: '1', name: 'Buck', breed: 'bulldog' },
19 },
20 },
21 },
22];
23
24it('renders without error', () => {
25 renderer.create(
26 <MockedProvider mocks={mocks} addTypename={false}>
27 <Dog name="Buck" />
28 </MockedProvider>,
29 );
30});
The mocks
array takes objects with specific request
s and their associated result
s. When the provider receives a GET_DOG_QUERY
with matching variables
, it returns the corresponding object from the result
key. A result
may alternatively be a function returning the object:
1const mocks = [
2 {
3 request: {
4 query: GET_DOG_QUERY,
5 variables: {
6 name: 'Buck',
7 },
8 },
9 result: () => {
10 // do something, such as recording that this function has been called
11 // ...
12 return {
13 data: {
14 dog: { id: '1', name: 'Buck', breed: 'bulldog' },
15 },
16 }
17 },
18 },
19];
Your mock request's variables object must exactly match the query variables sent from your component.
addTypename
You may notice the prop being passed to the MockedProvider
called addTypename
. The reason this is here is because of how Apollo Client normally works. When a request is made with Apollo Client normally, it adds a __typename
field to every object type requested. This is to make sure that Apollo Client's cache knows how to normalize and store the response. When we're making our mocks, though, we're importing the raw queries without typenames from the component files.
If we don't disable the adding of typenames to queries, the imported query won't match the query actually being run by the component during our tests.
In short, if queries are lacking
__typename
, it's important to pass theaddTypename={false}
prop to theMockedProvider
s.
Testing loading states
In this example, the Dog
component will render, but it will render in a loading state, not the final response state. This is because MockedProvider
doesn't just return the data but instead returns a Promise
that will resolve to that data. By using a Promise
it enables testing of the loading state in addition to the final state:
1it('should render loading state initially', () => {
2 const component = renderer.create(
3 <MockedProvider mocks={[]}>
4 <Dog />
5 </MockedProvider>,
6 );
7
8 const tree = component.toJSON();
9 expect(tree.children).toContain('Loading...');
10});
This shows a basic example test that tests the loading state of a component by checking that the children of the component contain the text Loading...
. In an actual application, this test would probably be more complicated, but the testing logic would be the same.
Testing final state
Loading state, while important, isn't the only thing to test. To test the final state of the component after receiving data, we can just wait for it to update and test the final state.
1const wait = require('waait');
2
3it('should render dog', async () => {
4 const dogMock = {
5 request: {
6 query: GET_DOG_QUERY,
7 variables: { name: 'Buck' },
8 },
9 result: {
10 data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
11 },
12 };
13
14 const component = renderer.create(
15 <MockedProvider mocks={[dogMock]} addTypename={false}>
16 <Dog name="Buck" />
17 </MockedProvider>,
18 );
19
20 await wait(0); // wait for response
21
22 const p = component.root.findByType('p');
23 expect(p.children).toContain('Buck is a poodle');
24});
Here, you can see the