Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

apollo's MockedProvider doesn't warn clearly about missing mocks

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 ?

like image 910
Quentin Avatar asked Feb 06 '20 17:02

Quentin


2 Answers

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);
});

like image 134
Quentin Avatar answered Nov 03 '22 15:11

Quentin


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:

1. Use a custom MockedProvider

This ensures globally that all errors will be logged. Can be noisy though if your tests expect some errors.

2. Log or handle errors in specific components

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'

3. Add logging in your mock to see if it was invoked

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.

4. Make sure variables match exactly

Your mock will only be invoked if the variables that the query or mutation were called with match the mock exactly.

5. When all else fails

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

like image 3
stephen.hanson Avatar answered Nov 03 '22 15:11

stephen.hanson