This article explores how to test React Hooks and outlines an eight-step testing plan you could employ to test your own projects.

Hooks were introduced in React 16.8 in late 2018. They are functions that hook into a functional component and allow us to use state and component features like componentDidUpdate, componentDidMount, and more. This was not possible before.

Also, hooks allow us to reuse component and state logic across different components. This was tricky to do before. Therefore, hooks have been a game-changer.

In this article, we will explore how to test React Hooks. We will pick a sufficiently complex hook and work on testing it.

We expect that you are an avid React developer already familiar with React Hooks. In case you want to brush up your knowledge, you should check out our tutorial, and here’s the link to the official documentation.

The Hook We Will Use for Testing

For this article, we will use a hook that I wrote in my previous article, Stale-while-revalidate Data Fetching with React Hooks. The hook is called useStaleRefresh. If you haven’t read the article, don’t worry as I will recap that part here.

This is the hook we will be testing:

import { useState, useEffect } from "react";
const CACHE = {};

export default function useStaleRefresh(url, defaultValue = []) {
  const [data, setData] = useState(defaultValue);
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {
    // cacheID is how a cache is identified against a unique request
    const cacheID = url;
    // look in cache and set response if present
    if (CACHE[cacheID] !== undefined) {
      setData(CACHE[cacheID]);
      setLoading(false);
    } else {
      // else make sure loading set to true
      setLoading(true);
      setData(defaultValue);
    }
    // fetch new data
    fetch(url)
      .then((res) => res.json())
      .then((newData) => {
        CACHE[cacheID] = newData;
        setData(newData);
        setLoading(false);
      });
  }, [url, defaultValue]);

  return [data, isLoading];
}

As you can see, useStaleRefresh is a hook that helps fetch data from a URL while returning a cached version of the data, if it exists. It uses a simple in-memory store to hold the cache.

It also returns an isLoading value that is true if no data or cache is available yet. The client can use it to show a loading indicator. The isLoading value is set to false when cache or fresh response is available.

A flowchart tracking the stale-while-refresh logic

At this point, I will suggest you spend some time reading the above hook to get a complete understanding of what it does.

In this article, we will see how we can test this hook, first using no test libraries (only React Test Utilities and Jest) and then by using react-hooks-testing-library.

The motivation behind using no test libraries, i.e., only a test runner Jest, is to demonstrate how testing a hook works. With that knowledge, you will be able to debug any issues that may arise when using a library that provides testing abstraction.

Defining the Test Cases

Before we begin testing this hook, let’s come up with a plan of what we want to test. Since we know what the hook is supposed to do, here’s my eight-step plan for testing it:

  1. When the hook is mounted with URL url1, isLoading is true and data is defaultValue.
  2. After an asynchronous fetch request, the hook is updated with data data1 and isLoading is false.
  3. When the URL is changed to url2, isLoading becomes true again and data is defaultValue.
  4. After an asynchronous fetch request, the hook is updated with new data data2.
  5. Then, we change the URL back to url1. The data data1 is instantly received since it is cached. isLoading is false.
  6. After an asynchronous fetch request, when a fresh response is received, the data is updated to data3.
  7. Then, we change the URL back to url2. The data data2 is instantly received since it is cached. isLoading is false.
  8. After an asynchronous fetch request, when a fresh response is received, the data is updated to data4.

The test flow mentioned above clearly defines the trajectory of how the hook will function. Therefore, if we can ensure this test works, we are good.

Test flow

Testing Hooks Without a Library

In this section, we will see how to test hooks without using any libraries. This will provide us with an in-depth understanding of how to test React Hooks.

To begin this test, first, we would like to mock fetch. This is so we can have control over what the API returns. Here is the mocked fetch.

function fetchMock(url, suffix = "") {
  return new Promise((resolve) =>
    setTimeout(() => {
      resolve({
        json: () =>
          Promise.resolve({
            data: url + suffix,
          }),
      });
    }, 200 + Math.random() * 300)
  );
}

This modified fetch assumes that the response type is always JSON and it, by default, returns the parameter url as the data value. It also adds a random delay of between 200ms and 500ms to the response.

If we want to change the response, we simply set the second argument suffix to a non-empty string value.

At this point, you might ask, why the delay? Why don’t we just return the response instantly? This is because we want to replicate the real world as much as possible. We can’t test the hook correctly if we return it instantly. Sure, we can reduce the delay to 50-100ms for faster tests, but let’s not worry about that in this article.

With our fetch mock ready, we can set it to the fetch function. We use beforeAll and afterAll for doing so because this function is stateless so we don’t need to reset it after an individual test.

// runs before any tests start running
beforeAll(() => {
  jest.spyOn(global, "fetch").mockImplementation(fetchMock);
});

// runs after all tests have finished
afterAll(() => {
  global.fetch.mockClear();
});

Then, we need to mount the hook in a component. Why? Because hooks are just functions on their own. Only when used in components can they respond to useState, useEffect, etc.

So, we need to create a TestComponent that helps us mount our hook.

// defaultValue is a global variable to avoid changing the object pointer on re-render
// we can also deep compare `defaultValue` inside the hook's useEffect
const defaultValue = { data: "" };

function TestComponent({ url }) {
  const [data, isLoading] = useStaleRefresh(url, defaultValue);
  if (isLoading) {
    return <div>loading</div>;
  }
  return <div>{data.data}</div>;
}

This is a simple component that either renders the data or renders a “Loading” text prompt if data is loading (being fetched).

Once we have the test component, we need to mount it on the DOM. We use beforeEach and afterEach to mount and unmount our component for each test because we want to start with a fresh DOM before each test.

let container = null;

beforeEach(() => {
  // set up a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

#testing #react #developer

Complete Guide to React Hooks Testing
2.05 GEEK