Adding a code-split mock GraphQL API to a simple React app

Let's add a mock GraphQL API to a React application for testing and development, code-splitting it out in order to ensure that bundle size doesn't grow for production builds.

I was rewriting an application of mine to use GraphQL while redesigning the frontend. This turned into enough work that I got bogged down pretty quickly; one would outpace the other. The application already had a stable database structure and REST API, so the work of converting it to GraphQL was a bit of a slog, while the redesign was quite challenging. I wanted to mock the GraphQL resolvers while I worked on the frontend, figuring I'd have to do it for testing eventually anyway.

Then I googled "GraphQL mocking" and found exactly that. What follows that excellent documentation is a guide on how to:

  • Mock a GraphQL backend with hand-written (not automatically mocked) resolvers for a simple application that doesn't use apollo-client
  • Code split the mocked backend out so you don't drag graphql-tools and graphql into production builds
  • Write a simple, but real test with react-testing-library

First, let's map out our GraphQL schema.

type Query {
  echo(str: String!): String!
}

Since you're not some cowboy coder and have hopefully, at this point in your career, acquired a healthy mistrust that anything will ever work, you'll want to achieve 100% code coverage of the paths leading to this highly intricate resolver.

To follow along at home, go ahead and create-react-app yourself an empty app to work with, then add the following dependencies:

yarn add graphql graphql-tools react-testing-library

First, we need to determine how we're actually going to interact with the GraphQL endpoints. Since this application isn't using apollo-client, we'll proceed as though we were wrapping fetch or perhaps a simple library like graphql-request. We'll also put our mock API into a separate file in order to take advantage of code splitting. Let's cleverly name that file mock-api.js

import { graphql } from 'graphql';
import { makeExecutableSchema } from 'graphql-tools';

/**
 * You'll notice I've defined the entire schema in this file.
 * In practice, you'd simply get the schema by introspection
 * or sharing code between your backend and frontend.
 */
const typeDefs = `
type Query {
  echo(str: String!): String!
}
`;

/**
 * I've defined resolvers here rather than use addMockFunctionsToSchema.
 * Not only do I think this will lead to more realistic tests, but my
 * goal is to use this mock frontend during development, so I want the
 * resolvers to return relatively realistic results.
 */
const resolvers = {
  Query: {
    echo: (param, { str }) => str
  }
}

const schema = makeExecutableSchema({ typeDefs, resolvers });

export const mockQuery = (queryString) => {
  return graphql(schema, queryString);
}

Now within our App.js, let's go ahead and replace the App component with a component that fires off a GraphQL query.

import React, { useEffect, useState } from 'react';
import { mockQuery as query } from './mock-api';

const App = () => {
  const [hello, setHello] = useState('loading');

  useEffect(() => {
    query(`{ echo(str: "hello world!") }`).then(result => setHello(result.data.echo));
  }, []);

  return (<p>{hello}</p>);
}

Now let's add a simple test in App.test.js to make sure this actually works.

import { render, waitForElement } from 'react-testing-library';

it('uses a GraphQL query', async () => {
  const { getByText } = render(<App />);
  await waitForElement(() => getByText("hello world!"));
})

A simple npm run test should verify that this works correctly.

Great! Now we're ready to deploy to production. Go ahead and yarn build to see what happens.

.

.

.

Crikey. For me, that adds about 68kb of unnecessary code to the bundle. How do we make it easy to sub in our mock API for tests and development, without pulling in these large dependencies in production? This is what I came up with; if anyone has a better solution, I'd love to hear it. Go ahead and add another api.js file

export const query = (queryString) => {
  if(process.env.REACT_APP_MOCK_API || process.env.NODE_ENV === 'test') {
    return import('./mock-api').then(module => module.mockQuery(queryString));
  }
  // This is where you would pass along parameters to fetch/graphql-request/etc for your
  // real calls
  throw new Error("API not implemented yet");
}

So, if you're running tests, or you've set REACT_APP_MOCK_API in the environment, requests will go to the mock backend, loaded on demand.

Then simply alter your App.js to use this wrapper. If you build again, you'll see two chunks, one containing the mock API dependencies, and the primary one containing your application.


Feel free to send questions, comments and suggestions to phil@upvalue.io