Posted by Erika McVey on July 18, 2022 | react, graphql

Unit tests are commonly a core component of a company’s product testing strategy. The fast feedback cycle and focused tests help to identify potential defects quickly, which in turn helps ensure the quality and reliability of the product.

Just because the feedback cycle is fast, it doesn’t mean the tests are always fast and easy to write. One issue that often contributed to difficulty writing front-end unit tests at Administrate was the ability to easily, clearly, and reliably mock HTTP requests in our tests.

The problem

Let’s say you’re using a traditional RESTful API to build a page that displays user profiles on a social media site. You’re displaying user details and a list of the posts they’ve made, so you’ll need to make 2 API requests.

export const ProfilePage: FunctionComponent<{
  id: string;
}> = ({ id }) => {
  const [user, setUser] = useState([]);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch(`https://myRESTfulAPI.com/users/${id}`)
      .then(res => res.json())
      .then(
        (result) => {
          setProfile(result);
        },
        (error) => {
          // handle error
        }
      )

    fetch(`https://myRESTfulAPI.com/posts?user={id}`)
      .then(res => res.json())
      .then(
        (result) => {
          setPosts(result);
        },
        (error) => {
          // handle error
        }
      )
  }, [])

  return (
    // render the user's details and list of posts
  )
}

When you’re writing tests, you can’t just mock the fetch function because it is used to retrieve data from 2 different endpoints. You can, however, implement some logic around the requests or use a library that does so. It might look something like this:

import { ProfilePage } from "./ProfilePage";
import { render } from "@testing-library/react";

global.fetch = jest.fn();

beforeEach(() => {
  fetch.mockClear();
});

it("renders the user info and posts", async () => {
  fetch.mockImplementation((url) => {
    if (url === "https://myRESTfulAPI.com/users/1") {
      // return mock user
    } else if (url === "https://myRESTfulAPI.com/posts?user=1") {
      // return mock posts
    }
  });

  const { container } = render(<ProfilePage id={1} />);
  // test expectations
});

Now let’s create the same page with a GraphQL API and useQuery. For the purposes of this example, we’re going to assume that we still have to make 2 requests, though that often isn’t the case when retrieving data from a GraphQL API.

import { USER_QUERY, POSTS_QUERY } from "./queries";
import { useQuery } from "@apollo/react-hooks";

export const ProfilePage: FunctionComponent<{
  id: string;
}> = ({ id }) => {
  const { data: userData, loading, error } = useQuery(USER_QUERY, {
    variables: { id },
  });
  const { data: postsData, loading, error } = useQuery(POSTS_QUERY, {
    variables: {
      filters: [
        {
          field: "author_id",
          operation: "eq",
          value: id,
        },
      ]
    },
  });


  return (
    // render the user's details and list of posts
  )
}

Since all requests go through the same endpoint, using the same mocking strategy we did with the RESTful API would be challenging. You’d have to try to parse the request body to determine which call is being made. That would be difficult to do with variables, filters, and verbose queries that may change over time. The approach would ultimately be difficult, hard to follow, and challenging to maintain. Luckily, there are a few other approaches we could take.

Option One: Apollo’s MockedProvider

Apollo offers the ability to solve this problem with a MockedProvider. This works by wrapping all your rendered test components in a provider and passing it a list of mocks. The mocks must exactly match the shape of the query and the variables the test case would use.

import { USER_QUERY, POSTS_QUERY } from "./queries";
import { render } from "@testing-library/react";
import { ProfilePage } from "./ProfilePage";

it("renders the user info and posts", async () => {
  const userMock = {
    request: {
      query: USER_QUERY,
      variables: { id: 1 },
    },
    result: {
      // mock user
    },
  };

  const postsMock = {
    request: {
      query: POSTS_QUERY,
      variables: {
        filters: [
          {
            field: "author_id",
            operation: "eq",
            value: id,
          },
        ]
     },
    result: {
      // mock posts
    },
  };


  const { container } = render(
    <MockedProvider mocks={[userMock, postsMock]}>
      <ProfilePage id={1} />
    </MockedProvider>,
  );

  // test expectations
});

While this approach is certainly easier than trying to parse queries ourselves, we found it still left a little to be desired. The setup is still verbose, and it isn’t easy to reuse mocks for different tests with different variables. You may find you have to mock requests even when you don’t care what data they return in a particular test. Additionally, if you have any logic around generating the query in your component, it isn’t easy to test since there is no way to spy on the MockedProvider.

Option Two: utilising data services

The approach we finally settled on required rearranging our code a bit, but we feel it makes request mocking significantly easier and more flexible. Instead of using useQuery directly in our components, we wrap the hook in a data service. We use one service per entity, but for this example, I will combine the service into one.

// profileDataService.ts

import { USER_QUERY, POSTS_QUERY } from "./queries";
import { QueryHookOptions, useQuery } from "@apollo/react-hooks";

export function useUserQuery(options?: QueryHookOptions) {
  return useQuery(USER_QUERY, options);
}

export function usePostsQuery(options?: QueryHookOptions) {
  return useQuery(POSTS_QUERY, options);
}

You could also include any filters or variable logic in your hooks if they’re commonly used. For most cases, we use the hooks just like useQuery and pass our variables in the traditional structure.

import { usePostsQuery, useUserQuery } from "./profileDataService";

export const ProfilePage: FunctionComponent<{
  id: string;
}> = ({ id }) => {
  const { data: userData, loading, error } = useUserQuery({
    variables: { id },
  });
  const { data: postsData, loading, error } = usePostsQuery({
    variables: {
      filters: [
        {
          field: "author_id",
          operation: "eq",
          value: id,
        },
      ]
    },
  });


  return (
    // render the user's details and list of posts
  )
}

Finally, we mock the service hooks in our unit tests.

import * as profileDataService from "./profileDataService";
import { render } from "@testing-library/react";
import { ProfilePage } from "./ProfilePage";

describe("profile page", () => {
  const userSpy = jest.spyOn(profileDataService, "useUserQuery");
  const postsSpy = jest.spyOn(profileDataService, "usePostsQuery");

  beforeEach(() => {
    userSpy.mockReset();
    postsSpy.mockReset();
  });

  it("renders the user info and posts", async () => {
    userSpy.mockReturnValue(//mock user);
    postsSpy.mockReturnValue(//mock posts);

    const { container } = render(<ProfilePage id={1} />);

    // test expectations
  }
});

Mocking the requests like this saves us from any complicated parsing logic to differentiate requests and it allows us to fully take advantage of jest’s mocking capabilities. Mocks can be set at the describe level and overwritten in individual tests, and you don’t have to return data if it won’t be used in a test.

Both approaches are good options, but we have found that this option is short and readable, allows us the most flexibility in mocking, and is easy to set up.