Unit Test custom React Hooks that rely on Promise

hook kozmyn

So you wrote a fancy custom hook that fetches some information from an API. How do we unit test such hooks?

Simple Hooks

Before we get to testing async hooks, you should already know how to test hooks. There are already some posts about it elsewhere (this one is an excellent example), so I will not repeat all that was already said there. We will use the react-hooks-testing-library for this purpose. Here’s an example from the library:

useCounter.js

import { useState, useCallback } from 'react'

function useCounter() {
  const [count, setCount] = useState(0)

  const increment = useCallback(() => setCount((x) => x + 1), [])

  return { count, increment }
}

export default useCounter

useCounter.test.js

import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from './useCounter'

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter())

  act(() => {
    result.current.increment()
  })

  expect(result.current.count).toBe(1)
})

Testing async hooks

Assume that instead of a local counter, we are fetching the count from a GraphQL API using Apollo Client’s query method. I know we could use Apollo’s react hook directly, but this is a contrived example as the async part could be anything.

useCounter.js

import { useQuery, useApolloClient } from "@apollo/react-hooks";
import { gql } from "graphql.macro";
import { useEffect } from "react";

const QUERY = gql`query Counter($id:ID!) { counter(id: $id) { count } }`;

export function useCounter(id) {
  const [count, setCount] = useState();
  const client = useApolloClient();
  useEffect(() => {
    client.query({
      query: QUERY,
      variables: { id }
    })
      .then(data => setCount(data.counter.count))
      .catch(error => console.log('failed to fetch count', error));
  }, [client, id]);
  return count;
}

In order to test this hook, we need a way to have a mocked result being passed to the query. First, we need to mock out the client and its query function. You need to change it to mock out your actual calls. I will mock out the query with a promise that resolves to some data after 100ms.

jest.mock('@apollo/react-hooks', () => {
  const client = {
    query: jest.fn(() => new Promise((resolve) => {
      setTimeout(() => resolve({ data: { counter: { count: 1 } } }), 100);
    })),
  };
  return {
    __esModule: true,
    useApolloClient: jest.fn(() => client),
  };
});

We created a promise that resolves after 100 ms. Now, we could wait for 100ms in our tests and then check the result after that time. But that doesn’t really work when we have a lot of tests or the promise could resolve before we get to the next statement if something takes time on the test framework. We will instead use fake timers. We will do this is a beforeEach to execute before each test in the file.

beforeEach(() => {
  jest.useFakeTimers();
  useApolloClient.mockClear();
});

Now to the actual test which is actually pretty self-explanatory.

it('fetches and resolves to a count', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useCounter('1');
  expect(result.current).toBeFalsy();
  expect(useApolloClient.mock.results[0].value.query).toHaveBeenCalled();

  act(() => jest.advanceTimersByTime(100));
  await waitForNextUpdate();

  expect(result.current).toEqual(1);
});
Published 6 Jun 2020

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter