BlogPost

How I'm testing my custom React Hook with Enzyme and Jest

I've been messing around with React Hooks for a good while in personal projects, the joy of personal projects is there's not too much need to fulfill testing requirements.

Then came along a shiny greenfield project at work. Not going into detail about that here but there is one detail you can probably guess, we used Hooks.

Disclaimer: I'm assuming you're all good with React, Hooks, Enzyme, and Javascript.

Disclaimer #2: Also, I'm not saying this is the number one best way to test custom hooks, just that this is how I found I can do it in the project I had.

So we have a tasty custom hook:

export const usePanda = () => {
  const [loading, setLoading] = React.useState(false);
  const [panda, setPanda] = React.useState(undefined);

  const getNewPanda = async () => {
    setLoading(true);

    setPanda(await new Promise(resolve => {
      setTimeout(() => {
        resolve(`/assets/${Math.ceil(Math.random() * 5)}.jpeg`);
      }, 500);
    }));

    setLoading(false);
  };

  return {
    getNewPanda,
    loading,
    panda
  };
};

Pretty simple really, we're pretending to be an API call to get a random Panda image, cause who doesn't love Pandas? So in our component, we can use our hook in our useEffect:

const { loading, panda, getNewPanda } = usePanda();

useEffect(() => {
  async function fetchData() {
    await getNewPanda();
  }
  fetchData();
}, []);

Here we've opted to implement our hook and perform our getNewPanda() call on the first mount.

So we have our hook in place and working, but how do we test our custom hook to safeguard any future unwanted changed? Let's have a gander...

The first run testing a custom hook didn't end too well. I got his with this message:

Invalid hook call. Hooks can only be called inside of the body of a function component.
- Jest

This happened as I tried to implement my hook like any other function in any other unit test:

it('failing test', () => {
  const { getNewPanda, loading, panda } = usePanda(); // Error thrown on first line
  getNewPanda();
  expect(panda).not.toEqual(undefined);
});

I hit the paths of Google looking for a solution, first result? The React docs. (hindsight - should've gone straight there)

You can only call Hooks while React is rendering a function component
- https://reactjs.org/warnings/invalid-hook-call-warning.html

So our problem was that we weren't calling our new panda hook in a real React function component.

This spurred me on to write a component in order to mount this panda hook. I hit despair - I could mount a component and our hook but then I couldn't get the mount to update with new values when the hook function was called. That was annoying.

That's when I stumbled across this Kent C Dodds video.

The above is a great video, I would recommend a watch. The biggest take away here was the difference in mounting components. Where Kent passes the hook as a child and initialises it, I was passing it as a prop which while mounted the hook, it didn't update the state as well (maybe I was doing something else wrong).

Minor niggle: The project I was working in wasn't using react-testing-library, we were using Enzyme.

So, I took the help from Kent and went about adjusting the mounting component which ended up like this:

export const mountReactHook = hook => {
  const Component = ({ children }) => children(hook());
  const componentHook = {};
  let componentMount;

  act(() => {
    componentMount = Enzyme.shallow(
      <Component>
        {hookValues => {
          Object.assign(componentHook, hookValues);
          return null;
        }}
      </Component>
    );
  });
  return { componentMount, componentHook };
};

Yes, this is remarkably similar to Kent's solution, just mount in a different way. That's why I'm here not taking credit for this overall solution.

So what we're doing here is accepting a hook, passing it as a child to a component which is mounted by Enzyme. When the mount occurs: Enzyme populates return values from the hook and mount.

Now we can call our hook within a nice controlled component in our tests:

describe("usePanda Hook", () => {
  let setupComponent;
  let hook;

  beforeEach(() => {
    setupComponent = mountReactHook(usePanda); // Mount a Component with our hook
    hook = setupComponent.componentHook;
  });

  it("sets loading to true before getting a new panda image", async () => {
    expect(hook.loading).toEqual(false);

    await act(async () => { // perform changes within our component
      hook.getNewPanda();
    });

    expect(hook.loading).toEqual(true); // assert the values change correctly

    await act(async () => {
      await wait(); // wait for the promise to resolve and next mount
    });

    expect(hook.loading).toEqual(false); // reassert against our values
  });

  it("sets a new panda image", async () => {
    expect(hook.panda).toEqual(undefined);

    await act(async () => {
      hook.getNewPanda();
      await wait();
    });

    expect(hook.panda).not.toEqual(undefined);
  });
});

The biggest takeaways from here are to remember to wrap our calls in 'acts' as we're essentially changing the component we need to tell the DOM that something's changing.

There we have it! A mounted custom React Hook in a testable way using Enzyme and Jest. I hope this helps you with your testing journey.