Testing a Custom React Hook
And gaining more confidence in your React Testing Library skills
Recently, I wrote a story about How to Build a Custom React Hook for Fetching Data. The implementation of the useFetch
is pretty straightforward, but it's a bit tricky to test properly.
We're going to use React Testing Library (RTL) with the assumption that you already have set up your testing environment.
Ok, let's just not talk in vain and jump straight to the code!
Creating the useFetch
A custom hook is a special type of function, in which you can use React hooks.
So here we're fetching data from the API in the useEffect
hook, putting them into state and returning. If an error occurs during fetch, we return the error.
But before anything else, with ref.current
we're checking if the current reference to the component is true. If it's not, it means the component is not mounted to the view. Therefore fetching data should not happen.
Testing custom hook
Now, we can cover the whole functionality of the useFetch
by testing whether:
- data are returned after a successful asynchronous call to the API
- an error is handled correctly when occurs
- call to the API is not happening when the
current
reference is falsy
For each point, we can write a separate test, right? So let's get started!
Setting up a testing file
Suppose we create a new file useFetch.test.js
and prepare a testing environment:
- We will definitely need mocked data since we're not going to really fetch them. Hence I created an array of
stubbedCountries
. - In
afterEach
block we clean up fetch implementations, whereas inafterAll
we restore the original implementation. This way we prevent tests to affect each other. More about this in jest docs.
Case 1: Getting the data
Ok, now let's talk about this step by step:
- As always, we firstly mock asynchronous call by returning resolved Promise with
stubbedCountries
. - Hooks are special types of functions because they can be used only within React components. Hence we need somehow to mock rendering hook in the component. For that, we can use
renderHook()
. - A callback of
renderHook()
is an execution of the hook with proper arguments. You can read about more options ofrenderHook()
. - After that, we call
waitForNextUpdate()
which returns a Promise that resolves the next time the hook renders, commonly when a state is updated as the result of an asynchronous update. - We expect the result (returned object) should strictly equal to the desired output.
Case 2: Error handling
This is pretty similar to the previous test:
- Again, we mock asynchronous call by returning Promise which must be with rejected status. It means something went wrong and the error should be caught.
- Same as above, we render our hook and call
waitForNextUpdate()
. - We expect the returned object should contain
error
and no data.
Case 3: Current reference is falsy
The React component which uses the useFetch
was in the meantime unmounted from the DOM and therefore fetching should not be triggered:
- Here we're just rendering the hook with given arguments. The second argument is
ref
, so it must be{ current: false }
. - In assertion, we expect that fetch has not been called. This means
useEffect
in the custom hook has not been called. - We also expect the returned object should not include data nor error.
Conclusion
In the end, this is what we were trying to achieve:
You can also check the test file in the GitHub repo.
However, what's important that we covered all possible cases with just 3 tests. This means, that coverage should be 100%, although I don't consider this aspect as relevant.
And the best thing is, that you can use this hook in every React project because the code is fully tested!
Want to learn more about testing? Feel free to read my other stories:
Unit testing with RTL
Automation (E2E) testing with Cypress
Thanks for reading!