We’re big on Apollo GraphQL here at Swarmia, but we’ve sometimes struggled with mocking. And based on these comments, it looks like we’re not alone. This blog post is here to help!
The problems arise for a few reasons:
Let’s go through five practical tips to make debugging Apollo MockedProvider more enjoyable.
Queries fail silently during a test case if the MockedProvider
doesn’t find a matching mock for the given request. You may eventually figure it out by console-logging the error returned by the useQuery
hook, but that requires additional digging.
Fortunately, we can use MockLink to add logging. I would link to the API docs but couldn’t find any. We have a VerboseMockedProvider
helper in our test utilities which we use as a replacement for MockedProvider
.
import { ApolloError, ApolloLink } from '@apollo/client'
import { onError } from '@apollo/client/link/error'
import {
MockedProvider,
MockedProviderProps,
MockedResponse
} from '@apollo/client/testing'
import { MockLink } from '@apollo/react-testing'
interface Props extends MockedProviderProps {
mocks?: ReadonlyArray<MockedResponse>
children?: React.ReactElement
}
const VerboseMockedProvider = (props: Props) => {
const { mocks = [], ...otherProps } = props
const mockLink = new MockLink(mocks)
const errorLoggingLink = onError(
({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(
({ message, locations, path }) =>
console.log(
'[GraphQL error]:' +
`Message: ${message},` +
`Location: ${locations},` +
`Path: ${path}`
)
)
}
if (networkError) {
console.log(`[Network error]: ${networkError}`)
}
}
)
const link = ApolloLink.from([errorLoggingLink, mockLink])
return (
<MockedProvider
{...otherProps}
addTypename={false}
mocks={mocks}
link={link}
/>
)
}
Using the helper, we can easily spot missing mocks from the CI logs or the local shell you’re using to run tests:
[Network error]: Error: No more mocked responses for the query:
query getDogParent {
parent {
id
name
}
}
Expected variables: {}
When seeing this error, we know that Apollo MockProvider
received query getDogParent
from a component but no matching mock was found. The phrase “Expected variables” can be interpreted in different ways but it means: “The attempted query had these variables: …”. Everything logged here refers to the attempted query, not to the mocks the MockedProvider does have.
We found out about this trick from a StackOverflow question, kudos to Quentin!
error
inside a componentThe previous tip helps to understand if an error with mocks exists in the first place. This second tip helps to dive deeper into the underlying problem.
Sometimes the additional logging within component context is the final piece of the puzzle. Let’s say you have this code in a component:
const [setFooMutation, { loading }] = useSetFooMutation()
You can log the error within a component:
const [setFooMutation, { error, loading }] =
useSetFooMutation()
console.log('got error from mutation:', error)
This helps to further narrow the context of the error and allows logging additional details like component props.
Update (April 2, 2024): Apollo released support for dynamic variable matching in Apollo Client 3.9. They should make variable matching easier and more flexible.
MockLink
is responsible for checking if a request is mocked. The logic it uses to detect matching mocks is pretty unforgiving. In practice, everything needs to match exactly:
mocks
array prop. Each attempted request that matches a mock, consumes the mocks that are left.Let’s say we have a mock like this:
import { SetExcludedPullRequestsDocument } from 'src/generated/graphql'
function mockSetExcludedPullRequests(ids: number[]) {
return {
request: {
query: SetExcludedPullRequestsDocument,
variables: { ids }
},
result: () => {
return {
data: {
setExcludedPullRequests: ids
}
}
}
}
}
We then mock the query in a test case:
<VerboseMockedProvider
mocks={[mockSetExcludedPullRequests([10, 11, 14])]}
>
And eventually the component calls the query with IDs in the wrong order:
await setExcludedPullRequestsMutation({
variables: { ids: [11, 10, 14] }
})
Boom. That little slip caused a “No more mocked responses for the query: ...” error. The same would happen if the component had accidentally used id
variable instead of ids
. The same would also happen if we’d called setExcludedPullRequestsMutation
twice, when having only a single mock definition. The possibilities for mistakes are endless.
The only solution is to make sure the mocked query has the exact same query and variables as what the component attempts to send. Sometimes you need a magnifying glass and diffing tools to spot these.
My preferred debugging approach is to log the mock query object (i.e. what mockSetExcludedPullRequests
returns), copy paste it to an editor and manually compare the mock request to the attempted query. If you’re brave enough, there’s a fork of @apollo/react-testing
which implements a jest-like diffing output for query variables.
This one sounds obvious but when you’re setting the mocks for MockedProvider
, make sure you’re using the correct mocks. The tip originates from a real issue that I once hit which caused a few hours of waste.
We have shared query mock creator utilities to make them reusable across test suites, for example mockGetSomething()
and mockSetSomething()
. The setter mock creator took zero parameters, so TypeScript didn’t complain about it either.
<VerboseMockedProvider
mocks={[
mockGetSomething(),
// The next item should've mocked the *setter*,
// not the getter
mockGetSomething()
]}
>
There’s nothing wrong about the creator utility pattern, the lesson here is to be more careful with the mock definitions.
One way to make sure that you’re calling the correct mock is to add logging to the result creator function:
import { SetSomethingDocument } from 'src/generated/graphql'
function mockSetSomething() {
return {
request: {
query: SetSomethingDocument
},
result: () => {
console.log('-- THIS SHOULD BE CALLED ')
return {
data: {
setSomething: true
}
}
}
}
}
If you don’t see the log output in stdout, then you know something weird is happening.
MockedProvider
is mountedIf multiple MockedProvider
components are mounted at the same time, the one that is deepest in the React component tree will be used for queries. This is an easy pitfall to step into when abstracting the numerous React context providers into utilities.
We often use helper components to reuse the set of providers that are required in all test cases. A test suite sometimes uses the same data and mocks for each individual case to avoid repetition. The helper component could look like this:
const Providers = ({ children }: ProviderProps) => (
<MemoryRouter>
{/* Mock requests relevant for the test cases */}
<VerboseMockedProvider mocks={[mockGetSomething()]}>
{children}
</VerboseMockedProvider>
</MemoryRouter>
)
Then, the actual test cases don’t need to repeat all the providers:
render(
<Providers>
<TestComponent />
</Providers>
)
But sometimes, one might accidentally use two or more overlapping provider abstractions. This leads to mounting multiple MockedProvider
components at the same time. This is what a simplified example would look like:
render(
<Providers>
<VerboseMockedProvider mocks={[mockGetAnotherThing()]}>
<TestComponent />
<VerboseMockedProvider>
</Providers>
)
When the TestComponent
attempts to call query getSomething
, it will fail to “No more mocked responses…” error because the inner VerboseMockedProvider
catches all requests before they reach the outer provider defined in Providers
component.
The simplest solution is to read through the whole React component tree in the test code and make sure there are no overlapping MockedProvider
components. An additional safety layer can be achieved by disallowing direct MockedProvider
imports using ESLint.
If you have any additional tips for Apollo mocking, let us know on Twitter. We’ll keep updating this post as we discover new ones and are more than happy to give you credit for contributing.
Subscribe to our newsletter
Get the latest product updates and #goodreads delivered to your inbox once a month.