React + GraphQL: defining undefined data in Unit Tests

You are a frontend developer (or maybe a full-stack developer doing some frontend now), and you were tasked to build just another React component that makes use of the shiny new GraphQL call – say, present details of the last credit card transaction to the end user.

Sure thing, you say, why not? The pattern is well-known and there are plenty of examples in the codebase to copy-paste from. Apollo Client provides you with it’s useful useQuery() hook, GraphQL query is conveniently generated for you by some home-grown or third-party tool and is ready to use. So you quickly end up with something like this:

function LastTransaction() {
  const { loading, data, error } = useQuery(gql(getLastTransaction));
  return (
    <>
      { loading && <Loader /> }
      { error && <Error /> }
      { data && <span>Last charge: {data.getLastTransaction.amount}</span> }
    </>
  );
}

You embed your new component in the app and it shows the transaction amount. So far, so good. Time to move on to the unit tests. Again, the pattern is known and no surprises expected. You have MockedProvider from Apollo Client, and you are well-aware that even the mocked client will take you through the loading stage of the component lifecycle – just need to waait a bit for the data to be rendered. Here it goes:

it('should fetch and display the last transaction', async () => {
  const mocks = [{
    request: { query: gql(getLastTransaction) },
    result: {
      data: {
        getLastTransaction: { 
          amount: '$42'
        }
      }
    }
  }];
  const wrapper = mount(
    <MockedProvider mocks={mocks} addTypename={false}>
      <LastTransaction />
    </MockedProvider>
  );
  expect(wrapper.find('Loader').length).toBe(1);
  await wait(0);
  expect(wrapper.find('Loader').length).toBe(0);
  expect(wrapper.find('span').at(0).text()).toContain('$42');
});

Here it gets more bumpy. Suddenly your test fails, with no data shown on the page.

Maybe your request is wrong, and it isn’t matched? You convert result to a function and expect it to be called. It does, so the request is matched, and the Loader is gone too. Time to switch to real debugging mode – and discover that the data is undefined in the main component! How come? All other similar tests are passing, and quick googling reveals nothing interesting or relevant…

It appears that the caveat is in the generated GraphQL query statement, which “conveniently” contains all the fields available on the getLastTransaction query. This includes fields that your new component is not using – and thus you did not bother to define in your mock data. However, Apollo Client still does it’s job to make sure that the data you get back matches your request. Note that this including null values for the nullable fields – that’s what you had supposedly requested, so Apollo Client is obliged to provide them back.

In our case, getLastTransaction query included merchant field, and once you add it to the mock the test works like a charm:

    result: {
      data: {
        getLastTransaction: { 
          merchant: 'Amazon',
          amount: '$42'
        }
      }

Another potential caveat in the generated queries lays in the future schema changes. Some day more fields may be added to the schema of the query you are using – for example, your backend people will realize that it makes sense to add a timestamp of the last transaction. The generated queries will be updated, and your unit tests will start failing again – this time with no frontend changes at all.

Leave a Reply