Testing custom react hooks that use fetch (or other async behaviour)

The custom hook

There are a few gotchas when testing custom hooks that have async behaviour, such as the fetch API. At the time of writing you have to use the alpha version (v16.9.0-alpha.0) of react, react-dom and react-test-renderer. Hopefully in the not too distant future this will be part of the stable react codebase.

Warning: An update to TestHook inside a test was not wrapped in act(...).When testing, code that causes React state updates should be wrapped into act(...):act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
in TestHook
in Suspense
import { useEffect, useState, useRef } from "react";

export const useDataApi = () => {
const firstUpdate = useRef(true);
const [error, setError] = useState(false);
const [apiUrl, setApiUrl] = useState("");
const [data, setData] = useState(null);

useEffect(
() => {
if (!firstUpdate.current) {
fetch(apiUrl)
.then(response => response.json())
.then(response => {
setData(response);
})
.catch(error => {
setError(true);
});
}
firstUpdate.current = false;
},
[apiUrl]
);
return { data, error, callApi: setApiUrl };
};
...
const { data, error, callApi } = useDataApi();
...
function handleClick(type, filterValue) {
callApi("your/data/endpoint/here.com");
}
...
return(
...
{data.whateverIsReturned}
...
)

Testing

My examples are using jest but you will need a few more libraries to get this to work.

Boilerplate

Your test file, without any tests should begin like this:

import React from "react";
import { useDataApi } from "path/to/hoo/useDataApi.jsx";
import "whatwg-fetch";
import { renderHook } from "@testing-library/react-hooks";
import fetchMock from "fetch-mock";
import { act } from "react-test-renderer";

describe("useDataApi", () => {
beforeAll(() => {
global.fetch = fetch;
});
afterAll(() => {
fetchMock.restore();
});
});

The tests

There are a few things to remember.

  • As fetch is async, we must use await act(async () => {...})

Testing a successful response

Gist link

import React from "react";
import { useDataApi } from "path/to/hoo/useDataApi.jsx";
import "whatwg-fetch";
import { renderHook } from "@testing-library/react-hooks";
import fetchMock from "fetch-mock";
import { act } from "react-test-renderer";

describe("useDataApi", () => {
beforeAll(() => {
global.fetch = fetch;
});
afterAll(() => {
fetchMock.restore();
});

it("should return data with a successful request", async () => {
const { result } = renderHook(() => useDataApi());
fetchMock.mock("test.com", {
returnedData: "foo"
});
await act(async () => {
result.current.callApi("test.com");
});

expect(result.current.data).toBe({
returnedData: "foo"
});
});
});

Testing an erroneous response

Gist link

import React from "react";
import { useDataApi } from "path/to/hoo/useDataApi.jsx";
import "whatwg-fetch";
import { renderHook } from "@testing-library/react-hooks";
import fetchMock from "fetch-mock";
import { act } from "react-test-renderer";

describe("useDataApi", () => {
beforeAll(() => {
global.fetch = fetch;
});
afterAll(() => {
fetchMock.restore();
});

it("should return error as true if api error", async () => {
const { result } = renderHook(() => useDataApi());

fetchMock.mock("test.com", 500);

await act(async () => {
result.current.callApi("test.com");
});

expect(result.current.data).toBe(null);
expect(result.current.error).toBe(true);
});
});

Freelance senior developer @ andrecalvo.co.uk. Brighton, UK.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store