Library to help unit testing when using apollo-client
npm install mock-apollo-clientHelps unit test components which use the Apollo Client.
| Version | Compatibility |
| ------- | ----------------------------------------------------------------------------------------------------------- |
| 0.x | Apollo client 2. README for 0.x here. |
| 1.x | Apollo client 3. README for 1.x here. |
| 2.x | Apollo client 4. (this README) |
When using the @apollo/client library, I ran into issues trying to unit test React components which called GraphQL queries and mutations.
The Apollo client library includes a MockedProvider component which allows query and mutation results to be mocked, but didn't offer enough control within unit tests. The Apollo client documentation for testing can be found here.
Specifically, some of the issues I faced were:
- Unable to assert queries/mutations were called with the expected variables
- Unable to assert how many times a specific query was called
- Unable to change the query/mutation result after the MockedProvider was initialised
- Unable to easily control the query/mutation loading state
The mock-apollo-client library helps with the above issues, by allowing more control within unit tests.
``bash`
npm install --save-dev mock-apollo-client
The examples below use React, Jest and React Testing Library, but mock-apollo-client is standalone and can used with any libraries and test frameworks.
The examples have been adapted from the official Apollo testing docs where possible.
Consider the file below, which contains a single GraphQL query and a component which is responsible for rendering the result of the query:
`jsx
// dog.jsx
import { gql } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
export const GET_DOG_QUERY = gql
query getDog($name: String) {
dog(name: $name) {
id
name
breed
}
};
export const Dog = ({ name }) => {
const { loading, error, data } = useQuery(GET_DOG_QUERY, {
variables: { name },
});
if (loading) return
Loading...
;{error.message}
; return (
{data.dog.name} is a {data.dog.breed}
To unit test this component using
mock-apollo-client, the test file could look like the following:`jsx
// dog.test.jsx
import '@testing-library/jest-dom';
import { ApolloProvider } from '@apollo/client/react';
import { render, screen } from '@testing-library/react';
import { createMockClient } from 'mock-apollo-client';import { GET_DOG_QUERY, Dog } from './dog';
it('renders the dog name and breed', async () => {
const mockClient = createMockClient();
mockClient.setRequestHandler(GET_DOG_QUERY, () =>
Promise.resolve({
data: { dog: { id: 1, name: 'Rufus', breed: 'Poodle' } },
}),
);
render(
,
);
expect(await screen.findByText('Rufus is a Poodle')).toBeInTheDocument();
});
`This test file does the following:
1. Instantiates a new mock Apollo client
1. Calls
setRequestHandler on the mock Apollo client instance to set a function to be called when Apollo client executes the Dog query
1. Passes the mock Apollo client instance to the ApolloProvider when rendering the component$3
The method
setRequestHandler is passed a function to call when Apollo client executes a given query and it is called with the variables for that query, so it is easy to assert the component is behaving as expected using a spy library.`javascript
const queryHandler = jest.fn().mockResolvedValue({
data: { dog: { id: 1, name: 'Rufus', breed: 'Poodle' } },
});mockApolloClient.setRequestHandler(GET_DOG_QUERY, queryHandler);
// ....
it('executes the query with the correct variables', () => {
expect(queryHandler).toHaveBeenCalledTimes(1);
expect(queryHandler).toHaveBeenCalledWith({ name: 'Rufus' });
});
`$3
A request handler returns a promise, so testing for loading state just requires that the promise returned is not resolved or rejected.
$3
To simulate a network error, the request handler should return a rejected promise. i.e.
`javascript
mockApolloClient.setRequestHandler(GET_DOG_QUERY, () =>
Promise.reject(new Error('Network Error')),
);
`To simulate GraphQL errors, the request handler should return a Promise which resolves with an
errors field. i.e.`javascript
mockApolloClient.setRequestHandler(GET_DOG_QUERY, () =>
Promise.resolve({ errors: [{ message: 'GraphQL Error' }] }),
);
`$3
Mutations can be tested the same way that queries are, by using
setRequestHandler and specifying a request handler for the mutation query.$3
Subscriptions can be tested, but require a different setup as they receive a stream of data.
Consider the file below, which contains a single subscription and a component which is responsible for rendering the updated data:
`jsx
// dogSubscription.jsximport { gql } from '@apollo/client';
import { useSubscription } from '@apollo/client/react';
export const DOG_SUBSCRIPTION = gql
;export const DogSubscription = ({ name }) => {
const { loading, error, data } = useSubscription(DOG_SUBSCRIPTION, {
variables: { name },
});
if (loading) return
Loading...
;
if (error) return Error!
; return (
{data.dog.name} has barked {data.dog.numberOfBarks} time(s)
);
};
`To unit test this component using
mock-apollo-client, the test file could look like the following:`jsx
// dogSubscription.test.jsximport '@testing-library/jest-dom';
import { act } from 'react';
import { ApolloProvider } from '@apollo/client/react';
import { render, screen } from '@testing-library/react';
import { createMockClient, createMockSubscription } from 'mock-apollo-client';
import { DOG_SUBSCRIPTION, DogSubscription } from './dogSubscription';
it('renders the dog details', async () => {
const mockClient = createMockClient();
const mockSubscription = createMockSubscription();
mockClient.setRequestHandler(DOG_SUBSCRIPTION, () => mockSubscription);
render(
,
);
act(() => {
mockSubscription.next({
data: { dog: { id: 1, name: 'Rufus', numberOfBarks: 0 } },
});
});
expect(
await screen.findByText('Rufus has barked 0 time(s)'),
).toBeInTheDocument();
act(() => {
mockSubscription.next({
data: { dog: { id: 1, name: 'Rufus', numberOfBarks: 1 } },
});
});
expect(
await screen.findByText('Rufus has barked 1 time(s)'),
).toBeInTheDocument();
});
`The subscription can be closed by calling
.complete if necessary for the test.#### Errors
You can also test error states by calling
.error on the mockSubscription and passing errors as described in Error States:`javascript
mockSubscription.error(new Error('Network Error'));
`#### Multiple subscriptions
A mock subscription will only be associated with a single invocation of a query. If a component is subscribing to the same query multiple times, then a separate mock subscription should be used for each one.
`javascript
const subscriptions = [];mockClient.setRequestHandler(
SUBSCRIBE_DOG_DOCUMENT,
() => {
const subscription = createMockSubscription();
subscriptions.push(subscription);
return subscription;
});
...
subscriptions.forEach((s) => s.next({ data: { dog: { id: 1, name: 'Rufus', numberOfBarks: 1 } } }));
`$3
#### Apollo client options
The
createMockClient method can be provided with the same constructor arguments that ApolloClient accepts which are used when instantiating the mock Apollo client.For example, to specify the cache (and possible types for fragment matching) that should be used:
`javascript
const cache = new InMemoryCache({
possibleTypes: myPossibleTypes,
});const mockClient = createMockClient({ cache });
`Note: it is not possible to specify the
link to use as this is how mock-apollo-client injects its request handling behaviour.#### Mock client options
Additionally, you can specify the following options which control the behaviour of the mock Apollo client:
| Option | Description | Default |
| ------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------- |
|
suppressMissingHandlerWarning | Controls whether a warning is logged when a request handler for an executed operation is not found. | false (Warning will be logged) |$3
If queries or mutations use fragments against union or interface types, you must inject a cache object when creating the mock client which has been provided with
possibleTypes, and also include the correct __typename field when mocking the response.For example:
`javascript
import { InMemoryCache } from '@apollo/client';
import { createMockClient } from 'mock-apollo-client';const cache = new InMemoryCache({
possibleTypes: {
Hardware: ['Memory', 'Cpu'],
},
});
const mockClient = createMockClient({ cache });
`You must then ensure that the query result includes the
__typename field as it would when calling your actual GraphQL API. This is to ensure that the fragment matching works as expected:`typescript
const query = gql;const mockData = {
hardware: {
__typename: 'Memory',
id: 2,
size: '16gb',
},
};
const requestHandler = jest.fn().mockResolvedValue({ data: mockData });
`$3
It's possible to remove a previously registered handler for a query using
removeRequestHandler.For example:
`javascript
mockApolloClient.removeRequestHandler(GET_DOG_QUERY);
``