JavaScriptT Light_870x220

Testing GraphQL doesn't have to be difficult. This article will explore static and dynamic mocks to make testing GraphQL a breeze.

Imagine you're working on a new feature with another team. You're in charge of the React side, but someone else is in charge of the GraphQL changes. Is it possible for you to develop your side before they have finished theirs?

Or how about wanting to test your components without making real GraphQL requests to the server? With Mocks, both are possible! Mocks allow you to provide fake responses to your queries, allowing you to fully test your components without interacting with a real server.

In this article, which assumes some previous knowledge of React and GraphQL, we will focus on two different ways to mock GraphQL query responses. The first is easier, but can be a little more rigid using MockedProvider. The second way allows us to define fake resolvers and generate our test data dynamically. Much of my inspiration for this article came from a talk given by Chang Wang at GraphQL Day Toronto.

The final codebase can be found here: https://github.com/leighhalliday/apollo-generating-types

What We're Testing

We will be working with Shopify's Storefront GraphQL API to show some products along with each product's images. The query to fetch this data looks like:

export const PRODUCTS_QUERY = gql`
  query ProductsData($preferredContentType: ImageContentType) {
    products(first: 10) {
      edges {
        node {
          id
          title
          images(first: 3) {
            edges {
              node {
                id
                transformedSrc(
                  maxWidth: 150
                  maxHeight: 100
                  preferredContentType: $preferredContentType
                )
              }
            }
          }
        }
      }
    }
  }
`;

The component which executes the above query and displays its results looks like:

export default function Products() {
  return (
    <ProductsQuery
      query={PRODUCTS_QUERY}
      variables={{ preferredContentType: ImageContentType.JPG }}
    >
      {({ data, loading, error }) => {
        if (error) {
          return  <div>Error loading products...</div>;
        }

        if (loading || !data) {
          return  <div>Loading products...</div>;
        }

        return (
          <div  data-testid="result">
            {data.products.edges.map(({ node: product }) => (
              <div key={product.id}>
                <h2>{product.title}</h2>
                <p>ID {product.id}</p>
                <ul className="images">
                  {product.images.edges.map(
                    ({ node: image }, index: number) => (
                      <li className="image-item" key={image.id || index}>
                        <img src={image.transformedSrc} />
                      </li>
                    )
                  )}
                </ul>
              </div>
            ))}
          </div>
        );
      }}
    </ProductsQuery>
  );
}

If you would like to learn more about working with TypeScript and Apollo GraphQL along with auto-generating types, please refer to this article.

Using MockedProvider

The first approach to mocking this GraphQL query is to use something called a MockedProvider. What it basically does is looks for a specific query, and, when it sees that query, uses a predefined response. You end up with an array of mocks, each with a request and its corresponding result.

In this case, I have imported the query PRODUCTS_QUERY from the file it is used in, ensuring I pass the same variable values used within the component we are testing (otherwise it won't match).

// imports required for code snippet below
import { ImageContentType } from "./generated/globalTypes";
import Products, { PRODUCTS_QUERY } from "./Products";

const mocks = [{
  request: {
    query: PRODUCTS_QUERY,
    variables: {
      preferredContentType: ImageContentType.JPG
    }
  },
  result: {
    data: {
      products: {
        edges: [{
          node: {
            id: "123",
            title: "Nike Shoes",
            images: {
              edges: [{
                node: {
                  id: "456",
                  transformedSrc: "https://www.images.com/shoe.jpg"
                }
              }]
            }
          }
        }]
      }
    }
  }
}];

It can get a bit tedious closing all of those objects and arrays, but the goal is to match the data structure exactly how you would expect to get it back from the server.

With Apollo, every time you use the Query component, for it to execute that query, it needs to be within a Provider. This provider gives the necessary context to resolve the queries being executed. This is where the MockedProvider comes into play. We'll wrap this provider around our component, allowing our mocks to resolve with fake data rather than making a real API call.

it("renders with MockedProvider", async () => {
  const { findByText, getByText } = render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <Products />
    </MockedProvider>
  );

  expect(getByText("Loading products...")).toBeInTheDocument();
  const productTag = await findByText("Nike Shoes");
  expect(productTag).toBeInTheDocument();
});

If react-testing-library is new to you, I wrote an introduction which may be useful.

Drawbacks of MockedProvider

While the MockedProvider allows you to get up and running quickly, it can be quite tedious defining all of your data for every test and scenario. If you wanted to simulate 15 products, you would need to define a large amount of mocked data, and then if you wanted to add an extra field, you would have to modify each of the 15 mocked products. This sort of thing grows tiring very quickly.

In the next section, we will try to overcome these drawbacks by utilizing a slightly more complicated approach, but one which comes with a number of improvements.

Dynamic Mocks

If the MockedProvider was a bit too rigid for your liking, you may be interested to know that there is a way to make mocks dynamic! In broad terms, this approach takes a GraphQL Schema (either manually defined or, as we will see, downloaded from the actual GraphQL API via an introspection query), and allows us to define mocked resolvers for each data type, with as much or as little control and overriding as we deem necessary.

Getting the Schema

The GraphQL Schema defines how the GraphQL API works: What queries and mutations can be performed and which types are defined? In this approach, we'll start by grabbing the GraphQL API's Schema which can be done using the schema:download command provided by the apollo package. What we will end up with is a schema.json file in the root of our project, containing the entire introspected output from the API.

yarn run apollo schema:download \
  --endpoint https://graphql.myshopify.com/api/graphql \
  --header "X-Shopify-Storefront-Access-Token: 078bc5caa0ddebfa89cccb4a1baa1f5c"

Creating the AutoMockedProvider

With the schema in hand, we have to define our own AutoMockedProvider. There are a large number of imports required for this functionality, but we'll dive into what they each do when they are needed.

import React, { ReactNode } from "react";
import { ApolloProvider } from "react-apollo";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { SchemaLink } from "apollo-link-schema";
import { makeExecutableSchema, addMockFunctionsToSchema, IMocks } from "graphql-tools";
import { printSchema, buildClientSchema } from "graphql/utilities";
import introspectionResult from "../../schema.json";

Next, we can define our AutoMockedProvider component. I have removed some of the TypeScript definitions to let the code read a little more cleanly, but if you are interested in TypeScript, I have left them in within the actual codebase on GitHub.

export default function AutoMockedProvider({ children, mockResolvers }) {
  // 1) Convert JSON schema into Schema Definition Language
  const schemaSDL = printSchema(
    buildClientSchema({ __schema: introspectionResult.__schema })
  );

  // 2) Make schema "executable"
  const schema = makeExecutableSchema({
    typeDefs: schemaSDL,
    resolverValidationOptions: {
      requireResolversForResolveType: false
    }
  });

  // 3) Apply mock resolvers to executable schema
  addMockFunctionsToSchema({ schema, mocks: mockResolvers });

  // 4) Define ApolloClient (client variable used below)
  const client = new ApolloClient({
    link: new SchemaLink({ schema }),
    cache: new InMemoryCache()
  });

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

With the AutoMockedProvider defined, we are able to use it as our Apollo Provider, but as we will see in the next section, this is just where the fun and flexibility begins.

<AutoMockedProvider>
  <Products />
</AutoMockedProvider>

Overriding Resolvers

Out of the box, the addMockFunctionsToSchema function provides default resolvers for all of the basic Scalar types that come with GraphQL (String, ID, Boolean, etc.). What this means is that, by default, a String will resolve to Hello World, and each other type has its own default value.

If a GraphQL API provides custom Scalar values or if you want to provide your own values, you can provide custom mock resolvers, allowing full flexibility over our AutoMockedProvider.

it("renders with AutoMockedProvider", async () => {
  const mockResolvers = {
    Product: () => ({ title: "Nike Shoes" }),
    URL: () =>  "https://www.shopify.com"
  };

  const { findByText, getByText } = render(
    <AutoMockedProvider mockResolvers={mockResolvers}>
      <Products />
    </AutoMockedProvider>
  );

  expect(getByText("Loading products...")).toBeInTheDocument();
  const productTag = await findByText("Nike Shoes");
  expect(productTag).toBeInTheDocument();
});

In this case, we have overriden what the title field of the Product type will be and have provided a resolver for the custom scalar type URL. An error will occur if a custom resolver is not provided for custom Scalar types.

Customizing Array Items with MockList

By default, any time there is an array of items, Apollo will return 2 of that item. But what if you want 0, 10, or even a variable amount of items? This is where the MockList object comes into play. It will allow us to define exactly how many of an item we want. In this case, we will have between 0 and 3 image edge items in our response.

const mockResolvers = {
  Product: () => ({
    title: "Nike Shoes",
    images: () => ({
      edges: () =>  new MockList([0, 3])
    })
  })
};

Accessing Arguments

Often our queries (and their fields) take arguments to provide additional detail to the server. In this query, the Shopify GraphQL API gives us the ability to define the image type (JPG, PNG, etc.) we would like as a response. Here is how you access these arguments, allowing you to customize your mocked resolver based on the arguments being passed to it.

const mockResolvers = {
  Image: () => ({
    transformedSrc: (root, { preferredContentType }) => `https://images.com/cat.${preferredContentType.toLowerCase()}`
  })
};

Now we can make the URL returned by the transformedSrc field's resolver have the extension matching the argument passed to the field (.jpg in this case).

Consistent Fake Values

Rather than defining every field, you may want to use a library such as faker to provide more realistic fake data. Sometimes your data is a little too random, though. Take the example below where we are using faker's uuid function to generate each ID, with the goal of producing a snapshot test. Every time the code runs, we will have unique UUIDs, making it difficult to have a consistent snapshot.

For this scenario, faker provides a way to define a seed value, ensuring that every time this code is executed, it will provide a random but consistent output faker.seed(123).

it("matches snapshot using seeds", async () => {
  faker.seed(123);
  const { findByTestId, asFragment } = render(
    <AutoMockedProvider
      mockResolvers={{
        URL: () => "https://www.shopify.com",
        ID: () => faker.random.uuid()
      }}
    >
      <Products />
    </AutoMockedProvider>
  );

  await findByTestId("result");
  expect(asFragment()).toMatchSnapshot();
});

Conclusion

In this article, we have seen two different ways to mock GraphQL queries for our tests. The first approach used the MockedProvider, allowing us to explicitly define which data will be returned for each query. This works well but can quickly become tedious and difficult to maintain.

The second approach involved creating an AutoMockedProvider, using the addMockFunctionsToSchema function from Apollo, allowing us to define and override resolvers for each data type and field, only having to explicitly define them when necessary. In my experience, this is the way to go, providing extreme flexibility with not too much additional overhead.


leigh-halliday
About the Author

Leigh Halliday

Leigh Halliday is a full-stack developer specializing in React and Ruby on Rails. He works for FlipGive, writes on his blog, and regularly posts coding tutorials on YouTube.

Related Posts

Comments

Comments are disabled in preview mode.