Hooks were introduced in React 16.8 in late 2018.

This was not possible before.

Also, hooks allow us to reuse component and state logic across different components.

A guide to testing Hooks for avid React developers

This was tricky to do before.

Therefore, hooks have been a game-changer.

In this article, we will explore how to test React Hooks.

Article image

We will pick a sufficiently complex hook and work on testing it.

We expect that you are anavid React developeralready familiar with React Hooks.

40% off TNW Conference!

A flowchart tracking the stale-while-refresh logic

The hook is calleduseStaleRefresh.

If you havent read the article, dont worry as I will recap that part here.

It uses a simple in-memory store to hold the cache.

Test flow

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.

Article image

Therefore, if we can ensure this test works, we are good.

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.

Article image

This is so we can have control over what the API returns.

Here is the mockedfetch.

It also adds a random delay of between 200ms and 500ms to the response.

Article image

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 dont we just return the response instantly?

Article image

This is because we want to replicate the real world as much as possible.

We cant test the hook correctly if we return it instantly.

With our fetch mock ready, we can set it to thefetchfunction.

Article image

Then, we need to mount the hook in a component.

Because hooks are just functions on their own.

Only when used in components can they respond touseState,useEffect, etc.

Article image

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

Once we have the test component, we need to mount it on the DOM.

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

Article image

initiate the test usingyarn test, and it works as expected.

Heres thecomplete code on GitHub.

Now, lets test when thisloadingtext changes to the fetched response data,url1.

Article image

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?

Article image

It will cover all possible wait times.

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

This is because the state update inuseStaleRefreshhook happens outsideact().

Article image

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).

Article image

As expected, it passes without errors.

Since we now know how to correctly wait for async changes, this should be easy.

Run this test, and it passes as well.

Article image

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.

Article image

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__.

Article image

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).

Now, lets take a quick look at optimization using helper methods.

Article image

The approach is not perfect but it works.

And yet, can we do better?

So, we are clearly wasting time here.

Article image

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.

Article image

Lets create awaitForfunction that does that.

Now, all this is great, but maybe we dont want to test the hook via UI.

Maybe we want to test a hook using its return values.

Article image

How do we do that?

It wont be difficult because we already have access to our hooks return values.

They are just inside the component.

Article image

If we can take those variables out to the global scope, it will work.

So lets do that.

We should also remove the destructuring in hooks return to make it more generic.

Article image

Thus, we have this updated test component.

Now the hooks return value is stored inresult, a global variable.

We can query it for our assertions.

Article image

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

At this point, we get the gist of testing React Hooks.

It should also render the hook in the test component and give us access to theresultvariable.

Article image

Lets see how we can do that.

First, we moveTestComponentandresultinside the function.

Using that, heres what we have.

Article image

We are calling this functionrenderHook.

Now, how do we go about updating the hook?

Since we are already using a closure, lets 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.

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 doesnt change.

We can fix that.

Testing using React-hooks-testing-library

React-hooks-testing-library does everything we have talked about before and then some.

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 dont 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 dont need to wrapact()around it.

Then, we can usererenderto update it with new props.

Notice we dont need to passdefaultValuehere.

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

Also tagged with