Published on

A Three Step Waltz for Mocking React Hooks with React Testing Library and TypeScript

Authors

Testing Hooks: The Three Step Waltz

Sometimes, you need to test a component that depends upon a hook. Reasonably, you probably want to mock out different return values within that suite. While the concept is straightforward, I've found the solution to be somewhat unintuitive. In fact, while I figured this pattern out over a year ago, I still have to find my old code every time to remember how to implement it.

I call it a Three Step Waltz:

  1. Mock the file with your hook in it
  2. Create a mock of the function itself + perform some type sorcery (one line of code so we'll call it one step 😉)
  3. MockImplementationOnce in your tests Optional: I like to include a "base case" mock return so that my test code only contains the variables I want to pay attention to + the rest can be handled via a spread operator. In my opinion, this makes it easier to tell what exactly is being tested where, as well as reducing human error.

Let's break down an example of this in practice. Here, I am testing a component called RunButton that starts an async process. I have a hook called useRunSomeFunc that triggers and tracks the process' state. I'd like to make sure that my button's content changes correctly based upon a few different hooks (one being this useRunSomeFunc):

jest.mock("hooks/useRunSomeFunc"); // 1. Mock the file
const mockUseRunSomeFunc = useRunSomeFunc as unknown as jest.Mock<Partial<typeof useRunSomeFunc>>; // 2. Create a mock of the function with some type sorcery
const baseHookValues = { // the optional base case
    completed: false
    runAsyncFunc: jest.fn(),
    loading: false,
};

"Teal," you're saying, "that's great, but you said three steps. Have you forgotten how to count? That's only two!" Ah ha, the third step comes in the actual tests where you can now manipulate this hook to your heart's content.

Example:

describe("RunButton", () => {

    beforeEach(() => jest.clearAllMocks())

    it("Shows the Incomplete label if completed is false", () => {
    mockUseRunSomeFunc.mockImplementationOnce(() => ({
     ...baseHookValues
     completed: false
    }));

    render(
        <RunButton />
    );
    const button = screen.getByRole("button", { name: "Incomplete" });
    expect(button).toBeInTheDocument();
    })

    it("Shows the Run Again label if completed is true", () => {
        mockUseRunSomeFunc.mockImplementationOnce(() => ({
        ...baseHookValues
        completed: true
        }));

        render(
            <RunButton />
        );
        const button = screen.getByRole("button", { name: "Run Again" });
        expect(button).toBeInTheDocument();
    })
})

Et voila!

If you'd like to learn more about testing with React, here are a few resources I've found useful: