Testing Hooks - Hooks for mass consumption

3 min read

This is part three in a series on Testing Hooks

The final category of hook usage is one of utility. These are hooks from libraries or hooks that are used widely throughout your code. They often have a large API (many parameters or returned values) so all the hooks functionality might not be used by a single component. This is when we might consider testing a hook in isolation.

This does not mean that we would avoid testing the components that use the hook too, this is also an important part of our code confidence.

Let's have another look at our hook and then figure out how to write tests for it in isolation:

import { useContext } from "react";
import { MultiselectContext } from "../MutliselectContext";

export default function useMultiselectOption({ title, value }) {
  const { onSelectOption, selectedValues, searchText } = useContext(
    MultiselectContext
  );
  const isOptionSelected = selectedValues.includes(value);
  const isHidden = searchText && !title.includes(searchText);

  return {
    onSelectOption,
    searchText,
    isOptionSelected,
    isHidden,
  };
}

One special consideration we need to make for this hook is the usage of Context. We will have to find a way to mock Context in our tests.

To test our hook in isolation we are going to use React Hooks Testing Library. It allows us to pass in a wrapper component that sets up the provider needed for Context.

First, let's create a Setup Function so we can run our hook with different properties for each of the tests.

import React from "react";
import { renderHook, act } from "@testing-library/react-hooks";
import { MultiselectContext } from "../MutliselectContext";
import useMultiselectOption from "./useMultiselectOption";

const onSelectOptionMock = jest.fn();
const selectedValues = [6, 5];

function setup({
  onSelectOption = () => null,
  selectedValues = [],
  searchText = "",
  option,
}) {
  const wrapper = ({ children }) => (
    <MultiselectContext.Provider
      value={{
        onSelectOption,
        selectedValues,
        searchText,
      }}
    >
      {children}
    </MultiselectContext.Provider>
  );

  return renderHook(() => useMultiselectOption(option), {
    wrapper,
  });
}

// ...

Then we can run the hook with the different parameters needed for each of our tests.

// ...

describe("when an option is selected", () => {
  it("then runs the callback", () => {
    const { result } = setup({
      onSelectOption: onSelectOptionMock,
      selectedValues,
      searchText: "",
      option: { title: "Six", value: 6 },
    });

    act(() => {
      result.current.onSelectOption(9);
    });

    expect(onSelectOptionMock).toHaveBeenCalledWith(9);
  });
});

describe("given an option is within the selected values", () => {
  it("then the option should be selected", () => {
    const { result } = setup({
      onSelectOption: onSelectOptionMock,
      selectedValues,
      searchText: "",
      option: { title: "Six", value: 6 },
    });

    expect(result.current.isOptionSelected).toBeTruthy();
  });
});

describe("given an option is not within the selected values", () => {
  it("then the option should not be selected", () => {
    const { result } = setup({
      onSelectOption: onSelectOptionMock,
      selectedValues,
      searchText: "",
      option: { title: "Eight", value: 8 },
    });

    expect(result.current.isOptionSelected).toBeFalsy();
  });
});

describe("given search text partially matches the title", () => {
  it("then the option should not be hidden", () => {
    const { result } = setup({
      onSelectOption: onSelectOptionMock,
      selectedValues,
      searchText: "Si",
      option: { title: "Six", value: 6 },
    });

    expect(result.current.isHidden).toBeFalsy();
  });
});

describe("given search text does not match the title", () => {
  it("then the option should be hidden", () => {
    const { result } = setup({
      onSelectOption: onSelectOptionMock,
      selectedValues,
      searchText: "Te",
      option: { title: "Six", value: 6 },
    });

    expect(result.current.isHidden).toBeTruthy();
  });
});

Writing tests like this where we test the hook in isolation makes our code more rigid. We would unlikely be able to change details in the hooks API without our tests breaking. When we tested our components without testing the hook we were free to change implementation details without reworking our tests. But in the case of a hook within a library or widely used within an app, it might be worth the trade-off of effort for high code confidence.

Was this article helpful?

Skill up in React testing with Robust UI