Unit test your React hook in minutes

ibrahimshamma99

Ibrahim Shamma

Posted on September 3, 2023

Unit test your React hook in minutes

Unit testing is a fundamental practice in software development, and it's particularly important for any software engineer.
It involves testing individual units or components of your code in isolation to ensure they work as expected.
In the context of React, these units often refer to individual functions, components, or small sections of your application's codebase.

There are two big gains for unit testing

  1. Living Documentation: Unit tests serve as a form of living documentation for your codebase. Instead of relying solely on comments or external documentation, unit tests provide concrete examples of how your code should behave. This documentation stays up-to-date as long as you maintain your tests alongside your code, and in the case of hooks these test cases will explore the different aspects of implementing the hook.

  2. Regression Prevention: unit tests act as a safety net for future changes. When you modify or extend your code, running unit tests can help you ensure that the existing functionality remains intact. If any of your changes break the expected behavior, the tests will catch it, allowing you to fix the issues before they reach production.

Implementation

We will now create a counter hook with history feature.

in your react app install the following package

npm i use-state-with-history 
Enter fullscreen mode Exit fullscreen mode

In case your not using create-react-app you need to install @testing-library/react

Now we create our hook

// src/use-counter.tsx

export function useCounterWithHistory(initialValue: number) {
  const [count, setCount, { backward, forward, go, history }] = useStateWithHistory<number>(initialValue);

  return [count, setCount, { backward, forward, go, history }];
}
Enter fullscreen mode Exit fullscreen mode

We now go to the fun the test cases:

// src/test/use-counter.test.tsx

import React from "react";
import { renderHook, screen } from "@testing-library/react";
import { useStateWithHistory } from "../use-counter";
import { act } from "react-dom/test-utils";

describe("Counter", () => {
  test("mounts a count of 0", async () => {
    const promise = Promise.resolve();
    const { result } = renderHook(() => useStateWithHistory<number>(0));
    expect(result.current[0]).toBe(0);
    await act(() => promise);
  });

  test("mounts a count of 0 and increments by one", async () => {
    const promise = Promise.resolve();
    const { result } = renderHook(() => useStateWithHistory<number>(0));
    act(() => result.current[1](result.current[0] + 1));
    expect(result.current[0]).toBe(1);
    await act(() => promise);
  });

  test("increments by one two times start with zero", async () => {
    const promise = Promise.resolve();
    const { result } = renderHook(() => useStateWithHistory<number>(0));
    act(() => {
      result.current[1](result.current[0] + 1);
    });

    act(() => {
      result.current[1](result.current[0] + 1);
    });
    expect(result.current[0]).toBe(2);
    await act(() => promise);
  });

  test("increments by one two times start with zero then goes back by 1 step", async () => {
    const promise = Promise.resolve();
    const { result } = renderHook(() => useStateWithHistory<number>(0));
    act(() => {
      result.current[1](result.current[0] + 1);
    });
    act(() => {
      result.current[1](result.current[0] + 1);
    });

    act(() => {
      result.current[2].backward();
    });
    expect(result.current[0]).toBe(1);
    await act(() => promise);
  });

  test("increments by one two times start with zero then goes back by 2 steps and one forward", async () => {
    const promise = Promise.resolve();
    const { result } = renderHook(() => useStateWithHistory<number>(0));
    act(() => {
      result.current[1](result.current[0] + 1);
    });
    act(() => {
      result.current[1](result.current[0] + 1);
    });

    act(() => {
      result.current[2].backward();
    });

    act(() => {
      result.current[2].backward();
    });

    act(() => {
      result.current[2].forward();
    });
    expect(result.current[0]).toBe(1);
    await act(() => promise);
  });

  test("increments by one two times start with zero then goes back by 2 steps and one forward then goes to 0", async () => {
    const promise = Promise.resolve();
    const { result } = renderHook(() => useStateWithHistory<number>(0));
    act(() => {
      result.current[1](result.current[0] + 1);
    });
    act(() => {
      result.current[1](result.current[0] + 1);
    });

    act(() => {
      result.current[2].backward();
    });

    act(() => {
      result.current[2].backward();
    });

    act(() => {
      result.current[2].forward();
    });
    act(() => {
      result.current[2].go(0);
    });
    expect(result.current[0]).toBe(0);
    await act(() => promise);
  });
});
Enter fullscreen mode Exit fullscreen mode

As you can see, we have many test cases, we want to test every possible use case of the hook, this can make sure we are preserving the living documentation target of the unit cases.

Also any change to the hook with well covering cases can make sure it is not breaking the existing hook cases.

Note: in the case of embedding events, you need to wrap them in act.

The result object is a react ref, so doing an act will mutate the internal state of hook.

Finally you can run the test cases using react-scripts

You can install it in case you are using remix, next or any custom template, but in case of create-react-app it is already installed, in case you have your own custom testing setup you can skip this part

npx react-scripts test
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
ibrahimshamma99
Ibrahim Shamma

Posted on September 3, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related