Mocking GraphQL API calls in React

Mocking GraphQL API calls in React

  • Beitrags-Autor:
  • Beitrags-Kommentare:0 Kommentare

This blog post focuses on mocking GraphQL query responses and describes how to write a unit test for a React component with Apollo Client using React Hooks, Jest, Apollo’s MockedProvider and React Testing Library. Some previous knowledge or familiarity with these is assumed.

Recently I’ve decided to face the challenge of testing our React components which are firing requests to our GraphQL API using Apollo Client, and creating context. Below is the code of our App component which represents the entry point of our application and it contains ApolloProvider that wraps AppointmentProvider component.

import React from 'react';
import { ApolloProvider } from '@apollo/react-hooks';
import { AppointmentProvider } from '../context/AppointmentContext';
import { AppointmentContainer } from './AppointmentContainer';
import { getClient } from '../graphql/apolloConfig';
import styles from './App.module.scss';

type AppProps = {
  id: string;
};

export function App({ id }: AppProps) {
  return (
      <div className={styles.app}>
        <ApolloProvider client={getClient()}>
          <AppointmentProvider id={id}>
              <AppointmentContainer />
          </AppointmentProvider>
        </ApolloProvider>
      </div>
  );
}

AppointmentProvider component comes from AppointmentContext object and allows AppointmentContainer component to subscribe to the context changes.

In this blog post I will focus on testing AppointmentContext with its Provider and mocking getAvailableSlots query which fetches available slots from a financial advisor’s calendar. The query looks like following:

import gql from 'graphql-tag';

export const getAvailableSlots = gql`
  query getAvailableSlots($id: ID!) {
    getAvailableSlots(id: $id) {
      start
      end
    }
  }
`;

Properties start and end are timestamps which define when an available slot begins and ends.

The above query is executed within a useGetAvailableSlotsQuery hook which returns an object from Apollo Client that contains properties: data, loading and error.

const { data, loading, error, refetch } =   useGetAvailableSlotsQuery({
    variables: {
      id,
    },
  });

Shown below is my first attempt to test our AppointmentContext with its Provider component.

import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { AppointmentContext } from './AppointmentContext';
import { AppointmentContainer } from '../container/AppointmentContainer';
import { GqlAppointment, GqlStatus, GqlConsultationType, GqlSex } from '../graphql/types';

const appointmentMock: GqlAppointment = {
  id: '1',
  status: GqlStatus.New,
  start: '2020-06-01T11:00:00+01:00',
  end: '2020-06-01T12:30:00+01:00',
  consultationType: GqlConsultationType.Tvo,
  appId: '12345',
  advisor: {
    firstName: 'Max',
    lastName: 'Mustermann',
    sex: GqlSex.Male,
    email: 'max@mustermann.de',
    phoneNumber: '+49 (89) 203057 1100',
    address: { street: 'Domagstraße 10', zip: '80809', city: 'München' },
  },
};

function setAppointmentState(
  data: GqlAppointment | null,
  loading: boolean,
  error: any,
  networkStatus: { isConnected: boolean } | null,
) {
  return { data, loading, error, networkStatus };
}

describe('<AppointmentContext />', () => {
  it('renders without crashing', () => {
    render(
      <AppointmentContext.Provider
        value={setAppointmentState(appointmentMock,
          false,
          null,
          { isConnected: true })}
      >
        <AppointmentContainer />
      </AppointmentContext.Provider>,
    );
  });
});

And that approach produced the following error:

Obviously, the error is thrown because Apollo Client isn’t available in the context and therefore the useQuery hook cannot consume it.

Apollo Client’s React integration relies on React’s context to pass the instance of Apollo Client through the React component tree. The suggested solution to wrap the root component in an ApolloProvider or pass an Apollo Client instance in via options was not a solution for us because that would have caused the tests to run against our actual backend. This wasn’t what we wanted.

Fortunately, there is a solution from Apollo Client itself and it is called MockedProvider.

The @apollo/client package exports a MockedProvider component which simplifies the testing of React components by mocking calls to the GraphQL endpoint. This allows the tests to be run in isolation and provides consistent results on every run by removing the dependence on remote data.

From Apollo Docs

MockedProvider provides mocks prop which makes it possible to mock a response of a certain query by specifying exact results that should be returned. Below is what our mocked response for getAvailableSlots query looks like.

export const mocks = [
  {
    request: {
      query: getAvailableSlots,
      variables: {
        id: '1',
      },
    },
    result: {
      data: {
        getAvailableSlots: [
          { start: '2020-06-01T11:00:00+01:00', end: '2020-06-01T12:30:00+01:00' },
          { start: '2020-06-01T12:30:00+01:00', end: '2020-06-01T14:00:00+01:00' },
          { start: '2020-06-02T14:30:00+01:00', end: '2020-06-02T16:00:00+01:00' },
          { start: '2020-06-04T12:30:00+01:00', end: '2020-06-04T14:00:00+01:00' },
          { start: '2020-06-04T14:00:00+01:00', end: '2020-06-04T15:30:00+01:00' },
          { start: '2020-06-04T15:30:00+01:00', end: '2020-06-04T17:00:00+01:00' },
        ],
      },
    },
  },
];

Here is the same test for the above component, but now using MockedProvider.

import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { AppointmentContext } from './AppointmentContext';
import { AppointmentContainer } from '../container/AppointmentContainer';
import { GqlAppointment } from '../graphql/types';
import { MockedProvider } from '@apollo/react-testing';
import { mocks } from '../graphql/mocks';

function setAppointmentState(
  data: GqlAppointment | null,
  loading: boolean,
  error: any,
  networkStatus: { isConnected: boolean; } | null,
) {
  return { data, loading, error, networkStatus, };
}

describe('<AppointmentContext />', () => {
  it('renders without crashing', () => {
    render(
      <MockedProvider mocks={mocks} addTypename={false}>
        <AppointmentContext.Provider
        value={setAppointmentState(null,
          false,
          null,
          { isConnected: true })}
      >
          <AppointmentContainer />
        </AppointmentContext.Provider>
      </MockedProvider>,
    );
  });
});

In the code above, there is additional two props in MockedProvider: addTypename and resolvers. It’s important to set the addTypename prop to false when using mocks because the queries are lacking a __typename field. Apollo Client normally adds that field to every object type when a request is made, so that its cache knows how to normalize and store the response. The second prop resolvers has an empty object passed into it because in our mock we don’t need to use local resolvers for testing.

Summary

In this blog post, I’ve presented an approach to mock GraphQL queries and test React components that use Apollo Client. Using MockedProvider it’s possible to explicitly define data that should be returned for each query.

Even though this approach worked well for our application and the amount of data that we have, it can get difficult to maintain it if the amount of data significantly increases. For that reason, it’s worth to mention a bit more complicated way of doing it, which involves dynamic mocks. This approach takes a GraphQL schema and allows us to define mocked resolvers for each data type. You can read more about it on Apollo Docs.

Schreibe einen Kommentar

6 − fünf =