Unit test your React hook in minutes
Ibrahim Shamma
Posted on September 3, 2023
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
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.
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
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 }];
}
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);
});
});
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 anact
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
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
October 26, 2023