I'm making unit tests for React components using apollo hooks (useQuery
, useMutation
), and in the tests I mock the actual queries with apollo's MockedProvider
. The problem is that sometimes, my mock doesn't match the query actually made by the component (either a typo when creating the mock, or the component evolves and changes some query variables). When this happens, MockedProvided
returns a NetworkError to the component. However in the test suite, no warning is displayed. This is frustrating, because sometimes my components do nothing with the error returned by useQuery
. This cause my tests, which used to pass, to suddenly fail silently, and gives me a hard time to find the cause.
This is an example of component using useQuery
:
import React from 'react';
import {gql} from 'apollo-boost';
import {useQuery} from '@apollo/react-hooks';
export const gqlArticle = gql`
query Article($id: ID){
article(id: $id){
title
content
}
}
`;
export function MyArticleComponent(props) {
const {data} = useQuery(gqlArticle, {
variables: {
id: 5
}
});
if (data) {
return (
<div className="article">
<h1>{data.article.title}</h1>
<p>{data.article.content}</p>
</div>
);
} else {
return null;
}
}
And this is a unit test, in which I made a mistake, because the variables object for the mock is {id: 6}
instead of {id: 5}
which will be requested by the component.
it('the missing mock fails silently, which makes it hard to debug', async () => {
let gqlMocks = [{
request:{
query: gqlArticle,
variables: {
/* Here, the component calls with {"id": 5}, so the mock won't work */
"id": 6,
}
},
result: {
"data": {
"article": {
"title": "This is an article",
"content": "It talks about many things",
"__typename": "Article"
}
}
}
}];
const {container, findByText} = render(
<MockedProvider mocks={gqlMocks}>
<MyArticleComponent />
</MockedProvider>
);
/*
* The test will fail here, because the mock doesn't match the request made by MyArticleComponent, which
* in turns renders nothing. However, no explicit warning or error is displayed by default on the console,
* which makes it hard to debug
*/
let titleElement = await findByText("This is an article");
expect(titleElement).toBeDefined();
});
How can I display an explicit warning in the console ?
I have submitted a Github issue to the apollo team, in order to suggest a built-in way to do this. Meanwhile, this is my homemade solution.
The idea is to give the MockedProvider
a custom apollo link. By default, it uses MockLink
initialized with the given mocks. Instead of this, I make a custom link, which is a chain formed of a MockLink
that I create the same way MockedProvider
would make it, followed by an apollo error link, which intercepts errors that may be returned by the request, and log them in the console. For this I create a custom provider MyMockedProvider
.
MyMockedProvider.js
import React from 'react'; import {MockedProvider} from '@apollo/react-testing'; import {MockLink} from '@apollo/react-testing'; import {onError} from "apollo-link-error"; import {ApolloLink} from 'apollo-link'; export function MyMockedProvider(props) { let {mocks, ...otherProps} = props; let mockLink = new MockLink(mocks); let errorLoggingLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) graphQLErrors.map(({ message, locations, path }) => console.log( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, ), ); if (networkError) console.log(`[Network error]: ${networkError}`); }); let link = ApolloLink.from([errorLoggingLink, mockLink]); return <MockedProvider {...otherProps} link={link} />; }
MyArticleComponent.test.js
import React from 'react'; import {render, cleanup} from '@testing-library/react'; import {MyMockedProvider} from './MyMockedProvider'; import {MyArticleComponent, gqlArticle} from './MyArticleComponent'; afterEach(cleanup); it('logs MockedProvider warning about the missing mock to the console', async () => { let gqlMocks = [{ request:{ query: gqlArticle, variables: { /* Here, the component calls with {"id": 5}, so the mock won't work */ "id": 6, } }, result: { "data": { "article": { "title": "This is an article", "content": "It talks about many things", "__typename": "Article" } } } }]; let consoleLogSpy = jest.spyOn(console, 'log'); const {container, findByText} = render( <MyMockedProvider mocks={gqlMocks}> <MyArticleComponent /> </MyMockedProvider> ); let expectedConsoleLog = '[Network error]: Error: No more mocked responses for the query: query Article($id: ID) {\n' + ' article(id: $id) {\n' + ' title\n' + ' content\n' + ' __typename\n' + ' }\n' + '}\n' + ', variables: {"id":5}'; await findByText('{"loading":false}'); expect(consoleLogSpy.mock.calls[0][0]).toEqual(expectedConsoleLog); });
It can be difficult to debug what goes wrong when Apollo's MockedProvider
doesn't return the expected response. Here are a few tips that I've stumbled onto:
MockedProvider
This ensures globally that all errors will be logged. Can be noisy though if your tests expect some errors.
If not using a custom MockedProvider
, you can still be alerted to missing mocks if you handle the error
that comes back from the query or mutation:
const { data, error } = useQuery(myQuery);
console.log(error); // 'no more mocked responses'
Your mock returns a result
object literal. result
can alternatively be a function, which only gets executed when your mock is invoked. You can log from this or add other instrumentation to verify that the query or mutation was executed.
Your mock will only be invoked if the variables that the query or mutation were called with match the mock exactly.
Sometimes all of the above goes fine but you still don't get data back from the query or mutation. This is almost certainly because the data that your mock returns does not match the expected format exactly. This will fail silently. I don't know yet how to troubleshoot this other than to verify your data format.
I've even had a case where my TypeScript code with Apollo-generated types was compiling, but I wasn't getting a response back because one of my types was slightly off (maybe I was sending extra fields, I don't remember).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With