TypeOfNaN

Mocking Apollo GraphQL Queries in React Testing

Nick Scialli February 17, 2021🚀🚀 7 minute read

If you're enjoying this blog, please consider:

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:

  1. Using the MockedProvider from the @apollo/client/testing library to wrap our component in the test
  2. 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:

top 10 javascript projects

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:

  1. Change our test function to be async so we can use await
  2. 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

Nick Scialli is a software engineer at the U.S. Digital Service.

Subscribe to the mailing list!

If you like what I post here, please sign up to get updates and code insights in your inbox. I won't spam you and you can unsubscribe any time!

Powered by Buttondown.