A guide to testing Hooks for avid React developers

The hook we will use for testing

For this article, we will use a hook that I wrote in my previous article,Stale-while-revalidate Data Fetching with React Hooks. The hook is calleduseStaleRefresh. If you haven’t read the article, don’t worry as I will recap that part here.

This is the hook we will be testing:

As you can see,useStaleRefreshis a hook that helps fetch data from a URL while returning a cached version of the data, if it exists. It uses a simple in-memory store to hold the cache.

It also returns anisLoadingvalue that is true if no data or cache is available yet. The client can use it to show a loading indicator. TheisLoadingvalue is set to false when cache or fresh response is available.

At this point, I will suggest you spend some time reading the above hook to get a complete understanding of what it does.

In this article, we will see how we can test this hook, first using no test libraries (only React Test Utilities and Jest) and then by usingreact-hooks-testing-library.

The motivation behind using no test libraries, i.e., only a test runnerJest, is to demonstrate how testing a hook works. With that knowledge, you will be able to debug any issues that may arise when using a library that provides testing abstraction.

Defining the test cases

Before we begin testing this hook, let’s come up with a plan of what we want to test. Since we know what the hook is supposed to do, here’s my eight-step plan for testing it:

The test flow mentioned above clearly defines the trajectory of how the hook will function. Therefore, if we can ensure this test works, we are good.

Testing hooks without a library

In this section, we will see how to test hooks without using any libraries. This will provide us with an in-depth understanding of how to test React Hooks.

To begin this test, first, we would like to mockfetch. This is so we can have control over what the API returns. Here is the mockedfetch.

This modifiedfetchassumes that the response type is always JSON and it, by default, returns the parameterurlas thedatavalue. It also adds a random delay of between 200ms and 500ms to the response.

If we want to change the response, we simply set the second argumentsuffixto a non-empty string value.

At this point, you might ask, why the delay? Why don’t we just return the response instantly? This is because we want to replicate the real world as much as possible. We can’t test the hook correctly if we return it instantly. Sure, we can reduce the delay to 50-100ms forfastertests, but let’s not worry about that in this article.

With our fetch mock ready, we can set it to thefetchfunction. We usebeforeAllandafterAllfor doing so because this function is stateless so we don’t need to reset it after an individual test.

Then, we need to mount the hook in a component. Why? Because hooks are just functions on their own. Only when used in components can they respond touseState,useEffect, etc.

So, we need to create aTestComponentthat helps us mount our hook.

This is a simple component that either renders the data or renders a“Loading”text prompt if data is loading (being fetched).

Once we have the test component, we need to mount it on the DOM. We usebeforeEachandafterEachto mount and unmount our component for each test because we want to start with a fresh DOM before each test.

Notice thatcontainerhas to be a global variable since we want to have access to it for test assertions.

With that set, let’s do our first test where we render a URLurl1, and since fetching the URL will take some time (seefetchMock), it should render “loading” text initially.

Run the test usingyarn test, and it works as expected. Here’s thecomplete code on GitHub.

Now, let’s test when thisloadingtext changes to the fetched response data,url1.

How do we do that? If you look atfetchMock, you see we wait for 200-500 milliseconds. What if we put asleepin the test that waits for 500 milliseconds? It will cover all possible wait times. Let’s try that.

The test passes, but we see an error as well (code).

This is because the state update inuseStaleRefreshhook happens outsideact(). To make sure DOM updates are processed timely, React recommends you useact()around every time a re-render or UI update might happen. So, we need to wrap our sleep withactas this is the time the state update happens. After doing so, the error goes away.

Now, run it again (code on GitHub). As expected, it passes without errors.

Let’s test the next situation where we first change the URL tourl2, then check theloadingscreen, then wait for fetch response, and finally check theurl2text. Since we now know how to correctly wait for async changes, this should be easy.

Run this test, and it passes as well. Now, we can also test the case where response data changes and the cache comes into play.

You will notice that we have an additional argumentsuffixin ourfetchMockfunction. This is for changing the response data. So we update our fetch mock to use thesuffix.

Now, we can test the case where the URL is set tourl1again. It first loadsurl1and thenurl1__. We can do the same forurl2, and there should be no surprises.

This entire test gives us the confidence that the hook does indeed work as expected (code). Hurray! Now, let’s take a quick look at optimization using helper methods.

Optimizing testing by using helper methods

So far, we have seen how to completely test our hook. The approach is not perfect but it works. And yet, can we do better?

Yes. Notice that we are waiting for a fixed 500ms for each fetch to be completed, but each request takes anything from 200 to 500ms. So, we are clearly wasting time here. We can handle this better by just waiting for the time each request takes.

How do we do that? A simple technique is executing the assertion until it passes or a timeout is reached. Let’s create awaitForfunction that does that.

This function simply runs a callback (cb) inside atry…catchblock every 10ms, and if thetimeoutis reached, it throws an error. This allows us to run an assertion until it passes in a safe manner (i.e., no infinite loops).

We can use it in our test as follows: Instead of sleeping for 500ms and then asserting, we use ourwaitForfunction.

Do it in all such assertions, and we can see a considerable difference in how fast our test runs (code).

Now, all this is great, but maybe we don’t want to test the hook via UI. Maybe we want to test a hook using its return values. How do we do that?

It won’t be difficult because we already have access to our hook’s return values. They are just inside the component. If we can take those variables out to the global scope, it will work. So let’s do that.

Since we will be testing our hook via its return value and not rendered DOM, we can remove the HTML render from our component and make it rendernull. We should also remove the destructuring in hook’s return to make it more generic. Thus, we have this updated test component.

Now the hook’s return value is stored inresult, a global variable. We can query it for our assertions.

After we change it everywhere, we can see our tests are passing (code).

At this point, we get the gist of testing React Hooks. There are a few improvements we can still make, such as:

We can do it by creating a factory function that has a test component inside it. It should also render the hook in the test component and give us access to theresultvariable. Let’s see how we can do that.

First, we moveTestComponentandresultinside the function. We will also need to pass Hook and the Hook arguments as function’s arguments so that they can be used in our test component. Using that, here’s what we have. We are calling this functionrenderHook.

The reason we haveresultas an object that stores data inresult.currentis because we want the return values to be updated as the test runs. The return value of our hook is an array, so it would have been copied by value if we returned it directly. By storing it in an object, we return a reference to that object so the return values can be updated by updatingresult.current.

Now, how do we go about updating the hook? Since we are already using a closure, let’s enclose another functionrerenderthat can do that.

The finalrenderHookfunction looks like this:

Now, we can use it in our test. Instead of usingactandrender, we do the following:

Then, we can assert usingresult.currentand update the hook usingrerender. Here’s a simple example:

Once you change it in all places, you will see it works without any problems (code).

Brilliant! Now we have a much cleaner abstraction to test hooks. We can still do better – for example,defaultValueneeds to be passed every time torerendereven though it doesn’t change. We can fix that.

But let’s not beat around the bush too much as we already have a library that improves this experience significantly.

Enterreact-hooks-testing-library.

Testing using React-hooks-testing-library

React-hooks-testing-library does everything we have talked about before and then some. For example, it handles container mounting and unmounting so you don’t have to do that in your test file. This allows us to focus on testing our hooks without getting distracted.

It comes with arenderHookfunction that returnsrerenderandresult. It also returnswait, which is similar towaitFor, so you don’t have to implement it yourself.

Here is how we render a hook in React-hooks-testing-library. Notice the hook is passed in the form of a callback. This callback is run every time the test component re-renders.

Then, we can test if the first render resulted inisLoadingas true and return value asdefaultValueby doing this. Exactly similar to what we implemented above.

To test for async updates, we can use thewaitmethod thatrenderHookreturned. It comes wrapped withact()so we don’t need to wrapact()around it.

Then, we can usererenderto update it with new props. Notice we don’t need to passdefaultValuehere.

Finally, the rest of the test will proceed similarly (code).

Wrapping up

My aim was to show you how to test React Hooks by taking an example of an async hook. I hope this helps you confidently tackle the testing of any kind of hook, as the same approach should apply to most of them.

I would recommend you use React-hooks-testing-library since it’s complete, and I haven’t run into significant problems with it thus far. In case you do encounter a problem, you now know how to approach it using the intricacies of testing hooks described in this article.

TheToptal Engineering Blogis a hub for in-depth development tutorials and new technology announcements created by professional software engineers in the Toptal network. You can read the original piece written byAvi Aryanhere. Follow the Toptal Engineering Blog onTwitterandLinkedIn.

Story byAvi Aryan