Docs
Launch GraphOS Studio

Testing React components

Using MockedProvider and associated APIs


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

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 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 , 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 and displays its result:

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

dog.test.js
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { MockedProvider } from "@apollo/client/testing";
import { GET_DOG_QUERY, Dog } from "./dog";
const mocks = []; // We'll fill this in next
it("renders without error", async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<Dog name="Buck" />
</MockedProvider>
);
expect(await screen.findByText("Loading...")).toBeInTheDocument();
});

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 . Let's define a mocked response for GET_DOG_QUERY when it's passed the name Buck:

dog.test.js
const mocks = [
{
request: {
query: GET_DOG_QUERY,
variables: {
name: "Buck"
}
},
result: {
data: {
dog: { id: "1", name: "Buck", breed: "bulldog" }
}
}
}
];

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

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

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

result: (variables) => { // `variables` is optional
// ...arbitrary logic...
return {
data: {
dog: { id: '1', name: 'Buck', breed: 'bulldog' },
},
}
},

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

Reusing mocks

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

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

Dynamic variables

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

For example, this mock will match all dog queries:

import { MockedResponse } from "@apollo/client/testing";
const dogMock: MockedResponse<Data, Variables> = {
request: {
query: GET_DOG_QUERY
},
variableMatcher: (variables) => true,
result: {
data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
},
};

This can also be useful for asserting specific individually:

import { MockedResponse } from "@apollo/client/testing";
const dogMock: MockedResponse<Data, Variables> = {
request: {
query: GET_DOG_QUERY
},
variableMatcher: jest.fn().mockReturnValue(true),
result: {
data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
},
};
expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({
name: 'Buck'
}));

Setting addTypename

In the example above, we set the addTypename prop of MockedProvider to false. This prevents from automatically adding the special __typename to every object it queries for (it does this by default to support data 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 that our mock is expecting.

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

Testing the "loading" and "success" states

To test how your component is rendered after its 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 the DOM elements containing the loading message first, followed by the success message "Buck is a poodle" (which appears after our completes):

it("should render dog", async () => {
const dogMock = {
delay: 30 // to prevent React from batching the loading state away
// delay: Infinity // if you only want to test the loading state
request: {
query: GET_DOG_QUERY,
variables: { name: "Buck" }
},
result: {
data: { dog: { id: 1, name: "Buck", breed: "poodle" } }
}
};
render(
<MockedProvider mocks={[dogMock]} addTypename={false}>
<Dog name="Buck" />
</MockedProvider>
);
expect(await screen.findByText("Loading...")).toBeInTheDocument();
expect(await screen.findByText("Buck is a poodle")).toBeInTheDocument();
});

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 errors.

  • Network errors are errors that occur while your client attempts to communicate with your .
  • errors are errors that occur while your attempts to resolve your client's .

Network errors

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

it("should show error UI", async () => {
const dogMock = {
request: {
query: GET_DOG_QUERY,
variables: { name: "Buck" }
},
error: new Error("An error occurred")
};
render(
<MockedProvider mocks={[dogMock]} addTypename={false}>
<Dog name="Buck" />
</MockedProvider>
);
expect(await screen.findByText("An error occurred")).toBeInTheDocument();
});

In this case, when the Dog component executes its , 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 errors, you define an errors inside a mock's result . The value of this field is an array of instantiated GraphQLError objects:

const dogMock = {
// ...
result: {
errors: [new GraphQLError("Error!")],
},
};

Because 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 's mutate function to actually execute the .

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 🐶):

delete-dog.jsx
import React from "react";
import { gql, useMutation } from "@apollo/client";
export const DELETE_DOG_MUTATION = gql`
mutation deleteDog($name: String!) {
deleteDog(name: $name) {
id
name
breed
}
}
`;
export function DeleteButton() {
const [mutate, { loading, error, data }] = useMutation(DELETE_DOG_MUTATION);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
if (data) return <p>Deleted!</p>;
return (
<button onClick={() => mutate({ variables: { name: "Buck" } })}>
Click to Delete Buck
</button>
);
}

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

delete-dog.test.js
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { MockedProvider } from "@apollo/client/testing";
import { DeleteButton, DELETE_DOG_MUTATION } from "./delete-dog";
it("should render without error", () => {
render(
<MockedProvider mocks={[]}>
<DeleteButton />
</MockedProvider>
);
});

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

The following test does execute the by clicking the button:

delete-dog.test.js
it("should render loading and success states on delete", async () => {
const deleteDog = { name: "Buck", breed: "Poodle", id: 1 };
const mocks = [
{
request: {
query: DELETE_DOG_MUTATION,
variables: { name: "Buck" }
},
result: { data: deleteDog }
}
];
render(
<MockedProvider mocks={mocks} addTypename={false}>
<DeleteButton />
</MockedProvider>
);
// Find the button element...
const button = await screen.findByText("Click to Delete Buck");
userEvent.click(button); // Simulate a click and fire the mutation
expect(await screen.findByText("Loading...")).toBeInTheDocument();
expect(await screen.findByText("Deleted!")).toBeInTheDocument();
});

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 , we use Testing Library's user-event library to simulate a click with its click method. This fires off the , 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 completed) before returning its result.

Testing error states for 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:

const cache = new InMemoryCache({
// ...configuration options...
})
<MockedProvider mocks={mocks} cache={cache}>
<DeleteButton />
</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.

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:

const {
mocks,
addTypename,
defaultOptions,
cache,
resolvers,
link,
showWarnings,
} = this.props;
const client = new ApolloClient({
cache: cache || new Cache({ addTypename }),
defaultOptions,
link: link || new MockLink(mocks || [], addTypename, { showWarnings }),
resolvers,
});

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

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

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

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

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

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

<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

Previous
Using TypeScript
Next
Mocking schema capabilities
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company