Another take on testing Custom React Hooks
Because as far as I can tell, right now, it’s a bit of a mess. (Me, a few days ago)
TL;DR;
- Unit-testing React Hooks in isolation is complicated (if it even works at all)
- It’s slow, and requires using a fake component to test the hook through it
- A better solution would be to completely mock the React Hooks API (useState, useEffect and so on), a mock you control.
- That’s possible today: it’s called Jooks!
What does Jooks cover? ✅
Custom React Hooks are functions that will themselves be used as hooks, and that are using other hooks. They, by convention, always start with “use”. For example, useMyHook().
Some examples of what is covered by this library and what isn’t:
Why Jooks?
As soon as I heard about React Hooks, I fell in love with them. Call that love at first sight! 💖
Playing with them in proof-of-concepts or personal projects was all fun and game, but trouble started when I created my first Custom Hook in a professional code base.
I wanted to unit-test my hook in isolation, and went into a rabbit hole of Medium articles on how I should do that, and none of them actually managed to solve my pretty basic problem:
Testing a hook that was simply using useState
and useEffect
to load some data from the server, storing that data in the state, and returning that data.
That hook looked pretty much like that:
Very simple example, that returns the fetched data along with some callback to fetch another set of data.
First, let’s see how we can test that with Jooks, then why the existing methods were not working.
The Solution 👍
I came to the conclusion that trying to test a hook by creating a fake component and bringing the whole React bloatware up just to test a simple function was slow, hard to setup, and felt downright wrong.
So my solution is to mock all the basic hooks and their behaviour (useState, useEffect and so on) and let you test your custom hook in perfect isolation and control.
First, install the library in your project:
yarn add jooks
With Jooks, all the basic hooks (useState, useEffect etc.) from React are mocked, with an implementation that replicates React’s behaviour.
It means that:
- You don’t need to create a fake component to test your hook
- You don’t have to bother with Enzyme or any React-focused testing library, just use Jest
- Your tests will be a lot quicker, as they don’t need React’s overhead
- You can see what’s really happening in more details, the hooks are less of a blackbox
Using Jooks is very simple: within your describe
function, call the init
function provided by Jooks, providing a callback to your hook:
const jooks = init(() => useMyCustomHook());
This will return a Jooks object, a wrapper, that will allow you to do 3 things:
- Run your hook (by calling
const results = jooks.run()
) - Wait for the hook to “mount” (i.e. fire its useEffect etc.) (
await jooks.mount();
) - Wait for anything else (if you called a function that would trigger some async side-effect). (
await jooks.wait();
)
Back to our example before, you can now test your hook like so:
Every React dependency you may have in your hook is now a mock and fully controlled by you, in a much lighter way than if it was handled by React itself. No component to instantiate, and full access to the hook’s internal state.
How it’s done today 🤕
If you follow these articles, you would be testing your Hook like so:
The problem is located in the useEffect
call: useEffect
takes a synchronous function, but inside it, we call an asynchronous one. It’s “fire and forget”.
The problem here is that if you are using all the recommended guides on how to test this, you will end up with one big problem: the act()
function, function that you are supposed to wrap any code that might mutate your state.
This act
function is unfortunately synchronous, so even if you instantiate your hook and it runs useEffect
within it, the call to setActivity will happen at a later stage outside of the act function, throwing an error in the console:
That problem is, as far as I know, unsolved.
And even if act()
becomes asynchronous one day, you’ll still have to deal with creating a fake component etc., so not doing that makes a lot of sense in terms of speed and convenience.
Is Jooks going to work for you?
There are a few gotchas:
- Since React is mocked, it means you don’t have access to the DOM at all. So this library won’t work if your hook is doing any DOM manipulation. That shouldn’t be a problem for 90%* of the use cases though.
- At the time of writing, only one hook was not implemented:
useImperativeHandle
. Since its use-case is very much DOM-centric, it might not be implemented at all. Wait & see!
PRs are welcome, so please don’t hesitate to report bugs or improvements on my GitHub: https://github.com/antoinejaussoin/jooks
Thanks for reading!
*: this number is completely made up.