So you wrote a fancy custom hook that fetches some information from an API. How do we unit test such 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)
})
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);
});