Mocking Apollo GraphQL Queries in React Testing
Nick Scialli
February 17, 2021
When I learn a new technology, the first thing I think is “okay, how do I use this?” But soon thereafter, the question becomes “how do I test this?”
That was the case recently with Apollo GraphQL in my React project. In this post, we’ll learn how to test a component that uses a GraphQL query.
TLDR: There are Two Keys to Getting this Right
The TLDR is that there are two keys to getting this right:
- Using the
MockedProvider
from the@apollo/client/testing
library to wrap our component in the test - Providing mocks that exactly match the query you’re trying to mock
An Example Component
Let’s create a very simple component. It’s going to use the readily-available github GraphQL API. I’ll already assume you have a React project set up with Apollo GraphQL installed.
Our query is basically going to search github for to top 10 repositories that match the query term “javascript.” The following component does just that!
import { useQuery, gql } from '@apollo/client';
export const TOP_PROJECTS = gql`
query SearchTopProjects($queryString: String!) {
search(query: $queryString, type: REPOSITORY, first: 10) {
edges {
node {
... on Repository {
name
description
stargazers {
totalCount
}
}
}
}
}
}
`;
export function TopProjects() {
const { loading, error, data } = useQuery(TOP_PROJECTS, {
variables: {
queryString: 'javascript',
},
});
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Oh no!</p>;
}
return (
<ul>
{data.search.edges.map(({ node }) => (
<li key={node.name}>
{node.description} | {node.stargazers.totalCount} Stars
</li>
))}
</ul>
);
}
Now if we were to see what this looks like in our browser, we hopefully get something like this:
Great! Now we have something to work with.
So… how do we test it?
Writing a Test
Let’s create a test file called TopProjects.test.jsx
. In this file, we’re going to use React Testing Library to render our component. Importantly, we’re going to wrap our component with the MockedProvider
exported from @apollo/client/testing
. The MockedProvider
takes a mocks
array as a property that represents all of the graphql calls we want to mock.
Here’s how our test might start out:
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { TopProjects } from './TopProjects';
const mocks = [];
test('renders top project list', () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<TopProjects />
</MockedProvider>
);
expect(container).toMatchSnapshot();
});
Here we’re just expecting our rendered HTML to match a snapshot. Of course, this won’t work because we haven’t mocked out our graphql call!
Mocking the GraphQL Call
The number one most important thing to do here is to make sure your GraphQL query and provided variables are exactly the same that your component is using. Remember, the great part about GraphQL is its flexibility, but we then have to make sure we’re being careful to duplicate the query when testing.
You may have noticed that we already exported the TOP_PROJECTS
graphql query from our TopProjects
file—this was no mistake! We can now use that query to help duplicate the request shape for our mock.
import { TopProjects, TOP_PROJECTS } from './TopProjects';
const mocks = [
{
request: {
query: TOP_PROJECTS,
variables: {
queryString: 'javascript',
},
},
result: {
data: {},
},
},
];
Notice that my query is the example GraphQL query exported from the component and the variables
provided match exactly what the component is asking for. This is critical since and deviation means the MockProvider
will think this is a totally different query!
So this mock isn’t quite done. We need to specify the data we’ll be getting in return! One helpful hack to specify the data is to look at your network tab in your browser and copy the response from the server. It’s pretty verbose, so I’ll just give you a preview:
{
"data": {
"search": {
"edges": [
{
"node": {
"name": "javascript",
"description": "JavaScript Style Guide",
"stargazers": {
"totalCount": 104737,
"__typename": "StargazerConnection"
},
"__typename": "Repository"
},
"__typename": "SearchResultItemEdge"
}
/* Plus 9 more nodes here */
],
"__typename": "SearchResultItemConnection"
}
}
}
So to mock this kind of response without having to write out a ton of lines, we’ll create a handy createRepo
function that will create each of the nodes for us. In the end, our test file looks something like this:
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { TopProjects, TOP_PROJECTS } from './TopProjects';
function createRepo(name, description, stars) {
return {
node: {
name,
description,
stargazers: {
totalCount: stars,
__typename: 'StargazerConnection',
},
__typename: 'Repository',
},
__typename: 'SearchResultItemEdge',
};
}
const mocks = [
{
request: {
query: TOP_PROJECTS,
variables: {
queryString: 'javascript',
},
},
result: {
data: {
search: {
edges: [
createRepo('js-stuff', 'Some JS stuff', 1000000),
createRepo('jsdoc', 'Make docs for JS', '900000'),
createRepo('anotherexample', 'Some othre description', '20000'),
],
__typename: 'SearchResultItemConnection',
},
},
},
},
];
test('renders learn react link', () => {
const { container, getByText } = render(
<MockedProvider mocks={mocks}>
<TopProjects />
</MockedProvider>
);
expect(container).toMatchSnapshot();
});
So let’s run the test with yarn test
(or npm if you use that).
yarn test
It looks like a snapshot was created in the __snapshots__
folder. Let’s check it out!
exports[`renders top project list 1`] = `
<div>
<p>
Loading...
</p>
</div>
`;
Oh no! We see our loading indicator. Well, this actually makes some sense as this is what our users will see when they initially load the app prior to our query resolving.
Handling the Asynchronous Behavior
To handle this asynchronous behavior, we do a couple things:
- Change our test function to be
async
so we can useawait
- Wait for the call stack to clear
Here’s how we do it.
import { render, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { TopProjects, TOP_PROJECTS } from './TopProjects';
function createRepo(name, description, stars) {
// Removed code for clarity
}
const mocks = [
// Removed code for clarity
];
test('renders top project list', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<TopProjects />
</MockedProvider>
);
await waitFor(() => new Promise((res) => setTimeout(res, 0)));
expect(container).toMatchSnapshot();
});
The idea behind the waitFor
line is that a setTimeout
callback, even with a 0 second timeout, will put the execution of the code in the event queue, thereby not being executed until the call stack clears. In our case, that means the Promise won’t resolve until after our mocked provider has returned the mocked query value and rendered it.
Let’s run our test again and check out our snapshot file:
xports[`renders top project list 1`] = `
<div>
<ul>
<li>
Some JS stuff
|
1000000
Stars
</li>
<li>
Make docs for JS
|
900000
Stars
</li>
<li>
Some othre description
|
20000
Stars
</li>
</ul>
</div>
`;
Ah-ha! Exactly what we want to see. We can now do whatever other assertions we migth want to do with react testing library and jest, knowing that we have full control over the mocked data.
Good luck!
Nick Scialli is a senior UI engineer at Microsoft.