Testing React components

Using MockedProvider and associated APIs


This article describes best practices for testing React components that use Apollo Client.

The examples below use Jest and React Testing Library, but the concepts apply to any testing framework.

The MockedProvider component

Every test for a React component that uses Apollo Client must make Apollo Client available on React's context. In application code, you achieve this by wrapping your component tree with the ApolloProvider component. In your tests, you use the MockedProvider component instead.

The MockedProvider component enables you to define mock responses for individual queries that are executed in your test. This means your test doesn't need to communicate with a GraphQL server, which removes an external dependency and therefore improves the test's reliability.

Example

Let's say we want to test the following Dog component, which executes a basic query and displays its result:

Click to expand 🐶
JavaScript
dog.jsx
1import React from "react";
2import { gql, useQuery } from "@apollo/client";
3
4// Make sure that both the query and the component are exported
5export const GET_DOG_QUERY = gql`
6  query GetDog($name: String) {
7    dog(name: $name) {
8      id
9      name
10      breed
11    }
12  }
13`;
14
15export function Dog({ name }) {
16  const { loading, error, data } = useQuery(GET_DOG_QUERY, {
17    variables: { name }
18  });
19  if (loading) return <p>Loading...</p>;
20  if (error) return <p>{error.message}</p>;
21  return (
22    <p>
23      {data.dog.name} is a {data.dog.breed}
24    </p>
25  );
26}

A basic rendering test for the component looks like this (minus mocked responses):

JavaScript
dog.test.js
1import "@testing-library/jest-dom";
2import { render, screen } from "@testing-library/react";
3import { MockedProvider } from "@apollo/client/testing";
4import { GET_DOG_QUERY, Dog } from "./dog";
5
6const mocks = []; // We'll fill this in next
7
8it("renders without error", async () => {
9  render(
10    <MockedProvider mocks={mocks} addTypename={false}>
11      <Dog name="Buck" />
12    </MockedProvider>
13  );
14  expect(await screen.findByText("Loading...")).toBeInTheDocument();
15});

Note: Usually, you import @testing-library/jest-dom in your test setup file, which provides certain custom jest matchers (such as toBeInTheDocument). The import is included in these examples for completeness.

Defining mocked responses

The mocks prop of MockedProvider is an array of objects, each of which defines the mock response for a single operation. Let's define a mocked response for GET_DOG_QUERY when it's passed the name Buck:

JavaScript
dog.test.js
1const mocks = [
2  {
3    request: {
4      query: GET_DOG_QUERY,
5      variables: {
6        name: "Buck"
7      }
8    },
9    result: {
10      data: {
11        dog: { id: "1", name: "Buck", breed: "bulldog" }
12      }
13    }
14  }
15];

Each mock object defines a request field (indicating the shape and variables of the operation to match against) and a result field (indicating the shape of the response to return for that operation).

Your test must execute an operation that exactly matches a mock's shape and variables to receive the associated mocked response.

Alternatively, the result field can be a function that returns a mocked response after performing arbitrary logic:

JavaScript
1result: (variables) => { // `variables` is optional
2  // ...arbitrary logic...
3
4  return {
5    data: {
6      dog: { id: '1', name: 'Buck', breed: 'bulldog' },
7    },
8  }
9},

Combining our code above, we get the following complete test:

Click to expand 🐶
JavaScript
dog.test.js
1import "@testing-library/jest-dom";
2import { render, screen } from "@testing-library/react";
3import { MockedProvider } from "@apollo/client/testing";
4import { GET_DOG_QUERY, Dog } from "./dog";
5
6const mocks = [
7  {
8    request: {
9      query: GET_DOG_QUERY,
10      variables: {
11        name: "Buck"
12      }
13    },
14    result: {
15      data: {
16        dog: { id: "1", name: "Buck", breed: "bulldog" }
17      }
18    }
19  }
20];
21
22it("renders without error", async () => {
23  render(
24    <MockedProvider mocks={mocks} addTypename={false}>
25      <Dog name="Buck" />
26    </MockedProvider>
27  );
28  expect(await screen.findByText("Loading...")).toBeInTheDocument();
29});

Reusing mocks

By default, a mock is only used once. If you want to reuse a mock for multiple operations, you can set the maxUsageCount field to a number indicating how many times the mock should be used:

Click to expand 🐶
JavaScript
dog.test.js
1import { GET_DOG_QUERY } from "./dog";
2
3const mocks = [
4  {
5    request: {
6      query: GET_DOG_QUERY,
7      variables: {
8        name: "Buck"
9      }
10    },
11    result: {
12      data: {
13        dog: { id: "1", name: "Buck", breed: "bulldog" }
14      }
15    },
16    maxUsageCount: 2, // The mock can be used twice before it's removed, default is 1
17  }
18];

Passing Number.POSITIVE_INFINITY will cause the mock to be reused indefinitely.

Dynamic variables

Sometimes, the exact value of the variables being passed are not known. The MockedResponse object takes a variableMatcher property that is a function that takes the variables and returns a boolean indication if this mock should match the invocation for the provided query. You cannot specify this parameter and request.variables at the same time.

For example, this mock will match all dog queries:

TypeScript
1import { MockedResponse } from "@apollo/client/testing";
2
3const dogMock: MockedResponse<Data, Variables> = {
4  request: {
5    query: GET_DOG_QUERY
6  },
7  variableMatcher: (variables) => true,
8  result: {
9    data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
10  },
11};

This can also be useful for asserting specific variables individually:

TypeScript
1import { MockedResponse } from "@apollo/client/testing";
2
3const dogMock: MockedResponse<Data, Variables> = {
4  request: {
5    query: GET_DOG_QUERY
6  },
7  variableMatcher: jest.fn().mockReturnValue(true),
8  result: {
9    data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
10  },
11};
12
13expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({
14  name: 'Buck'
15}));

Setting addTypename

In the example above, we set the addTypename prop of MockedProvider to false. This prevents Apollo Client from automatically adding the special __typename field to every object it queries for (it does this by default to support data normalization in the cache).

We don't want to automatically add __typename to GET_DOG_QUERY in our test, because then it won't match the shape of the query that our mock is expecting.

Unless you explicitly configure your mocks to expect a __typename field, always set addTypename to false in your tests.

Testing the "loading" and "success" states

To test how your component is rendered after its query completes, Testing Library provides several findBy methods. From the Testing Library docs:

findBy queries work when you expect an element to appear but the change to the DOM might not happen immediately.

We can use the asynchronous screen.findByText method to query the DOM elements containing the loading message first, followed by the success message "Buck is a poodle" (which appears after our query completes):

JavaScript
1it("should render dog", async () => {
2  const dogMock = {
3    delay: 30 // to prevent React from batching the loading state away
4    // delay: Infinity // if you only want to test the loading state
5
6    request: {
7      query: GET_DOG_QUERY,
8      variables: { name: "Buck" }
9    },
10    result: {
11      data: { dog: { id: 1, name: "Buck", breed: "poodle" } }
12    }
13  };
14  render(
15    <MockedProvider mocks={[dogMock]} addTypename={false}>
16      <Dog name="Buck" />
17    </MockedProvider>
18  );
19  expect(await screen.findByText("Loading...")).toBeInTheDocument();
20  expect(await screen.findByText("Buck is a poodle")).toBeInTheDocument();
21});

Testing error states

Your component's error states are just as important to test as its success state, if not more so. You can use the MockedProvider component to simulate both network errors and GraphQL errors.

  • Network errors are errors that occur while your client attempts to communicate with your GraphQL server.

  • GraphQL errors are errors that occur while your GraphQL server attempts to resolve your client's operation.

Network errors

To simulate a network error, you can include an error field in your test's mock object, instead of the result field:

JavaScript
1it("should show error UI", async () => {
2  const dogMock = {
3    request: {
4      query: GET_DOG_QUERY,
5      variables: { name: "Buck" }
6    },
7    error: new Error("An error occurred")
8  };
9  render(
10    <MockedProvider mocks={[dogMock]} addTypename={false}>
11      <Dog name="Buck" />
12    </MockedProvider>
13  );
14  expect(await screen.findByText("An error occurred")).toBeInTheDocument();
15});

In this case, when the Dog component executes its query, the MockedProvider returns the corresponding error. This applies the error state to our Dog component, enabling us to verify that the error is handled gracefully.

GraphQL errors

To simulate GraphQL errors, you define an errors field inside a mock's result field. The value of this field is an array of instantiated GraphQLError objects:

JavaScript
1const dogMock = {
2  // ...
3  result: {
4    errors: [new GraphQLError("Error!")],
5  },
6};

Because GraphQL supports returning partial results when an error occurs, a mock object's result can include both errors and data.

Testing mutations

You test components that use useMutation similarly to how you test components that use useQuery. Just like in your application code, the primary difference is that you need to call the mutation's mutate function to actually execute the operation.

Example

The following DeleteButton component executes the DELETE_DOG_MUTATION to delete a dog named Buck from our graph (don't worry, Buck will be fine 🐶):

JavaScript
delete-dog.jsx
1import React from "react";
2import { gql, useMutation } from "@apollo/client";
3
4export const DELETE_DOG_MUTATION = gql`
5  mutation deleteDog($name: String!) {
6    deleteDog(name: $name) {
7      id
8      name
9      breed
10    }
11  }
12`;
13
14export function DeleteButton() {
15  const [mutate, { loading, error, data }] = useMutation(DELETE_DOG_MUTATION);
16
17  if (loading) return <p>Loading...</p>;
18  if (error) return <p>Error!</p>;
19  if (data) return <p>Deleted!</p>;
20
21  return (
22    <button onClick={() => mutate({ variables: { name: "Buck" } })}>
23      Click to Delete Buck
24    </button>
25  );
26}

We can test the initial rendering of this component just like we tested our Dog component:

JavaScript
delete-dog.test.js
1import '@testing-library/jest-dom';
2import userEvent from '@testing-library/user-event';
3import { render, screen } from '@testing-library/react';
4import { MockedProvider } from "@apollo/client/testing";
5import { DeleteButton, DELETE_DOG_MUTATION } from "./delete-dog";
6
7it("should render without error", () => {
8  render(
9    <MockedProvider mocks={[]}>
10      <DeleteButton />
11    </MockedProvider>
12  );
13});

In the test above, DELETE_DOG_MUTATION is not executed, because the mutate function is not called.

The following test does execute the mutation by clicking the button:

JavaScript
delete-dog.test.js
1it("should render loading and success states on delete", async () => {
2  const deleteDog = { name: "Buck", breed: "Poodle", id: 1 };
3  const mocks = [
4    {
5      request: {
6        query: DELETE_DOG_MUTATION,
7        variables: { name: "Buck" }
8      },
9      result: { data: deleteDog }
10    }
11  ];
12
13  render(
14    <MockedProvider mocks={mocks} addTypename={false}>
15      <DeleteButton />
16    </MockedProvider>
17  );
18
19  // Find the button element...
20  const button = await screen.findByText("Click to Delete Buck");
21  userEvent.click(button); // Simulate a click and fire the mutation
22
23  expect(await screen.findByText("Loading...")).toBeInTheDocument();
24  expect(await screen.findByText("Deleted!")).toBeInTheDocument();
25});

Again, this example is similar to the useQuery-based component above, but it differs after the rendering is completed. Because this component relies on a button click to fire a mutation, we use Testing Library's user-event library to simulate a click with its click method. This fires off the mutation, and the rest of the test runs as expected.

Remember that the mock's value for result can also be a function, so you can perform arbitrary logic (like setting a boolean to indicate that the mutation completed) before returning its result.

Testing error states for mutations is identical to testing them for queries.

Testing with the cache

If your application sets any cache configuration options (such as possibleTypes or typePolicies), you should provide MockedProvider with an instance of InMemoryCache that sets the exact same options:

JavaScript
1const cache = new InMemoryCache({
2  // ...configuration options...
3})
4
5<MockedProvider mocks={mocks} cache={cache}>
6  <DeleteButton />
7</MockedProvider>,

The following sample specifies possibleTypes and typePolicies in its cache configuration, both of which must also be specified in relevant tests to prevent unexpected behavior.

Click to expand 🐶
JavaScript
1// "Dog" supertype can be of type "ShibaInu"
2const ShibaFragment = gql`
3  fragment ShibaInuFields on Dog {
4    ... on ShibaInu {
5      tail {
6        isCurly
7      }
8    }
9  }
10`;
11
12export const GET_DOG_QUERY = gql`
13  query GetDog($name: String) {
14    dog(name: $name) {
15      id
16      name
17      breed
18
19      ...ShibaInuFields
20    }
21  }
22
23  ${ShibaFragment}
24`;
25
26export const cache = new ApolloClient({
27  cache: new InMemoryCache({
28    possibleTypes: {
29      Dog: ["ShibaInu"],
30    },
31    // suppose you want you key fields for "Dog" to not be simply "id"
32    typePolicies: {
33      keyFields: {
34        Dog: ["name", "breed"],
35      },
36    },
37  }),
38});

Testing local state

In order to properly test local state using MockedProvider, you'll need to pass a configured cache into MockedProvider itself.

MockedProvider creates its own ApolloClient instance behind the scenes like this:

JavaScript
1const {
2  mocks,
3  addTypename,
4  defaultOptions,
5  cache,
6  resolvers,
7  link,
8  showWarnings,
9} = this.props;
10const client = new ApolloClient({
11  cache: cache || new Cache({ addTypename }),
12  defaultOptions,
13  link: link || new MockLink(mocks || [], addTypename, { showWarnings }),
14  resolvers,
15});

Therefore if you're using Apollo Client 2.x local resolvers, or Apollo Client 3.x type/field policies, you have to tell the MockedProvider component what you're going to do with @client fields. Otherwise the ApolloClient instance created behind the scenes doesn't know how to handle your tests.

If using Apollo Client 2.x local resolvers, make sure your resolvers object is passed into MockedProvider:

JavaScript
1<MockedProvider mocks={mocks} resolvers={resolvers} ...

If using Apollo Client 3.x type/field policies, make sure your configured cache instance (with your typePolicies) is passed into MockedProvider:

JavaScript
1<MockedProvider mocks={mocks} cache={cache} ...

If you're using Apollo Client 2.x local resolvers, you also need to pass your resolver map:

JavaScript
1<MockedProvider mocks={mocks} cache={cache} resolvers={resolvers} ...

This is necessary because otherwise, the MockedProvider component doesn't know how resolve local-only fields in your queries.

Sandbox example

For a working example that demonstrates how to test components, check out this project on CodeSandbox:

Edit Testing React Components

Feedback

Edit on GitHub

Forums