JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux

JavaScript React Testing

The hooks are an exciting addition to React and undoubtedly one that helps us to separate the logic from the template. Doing so makes the above logic more testable. Unfortunately, testing hooks does not prove to be that straightforward. In this article, we look into how we can deal with it using react-hooks-testing-library.

Identifying the tricky part

To understand what makes testing React hooks problematic, let’s create a simple custom hook. We will base it on a hook from The Facade pattern and applying it to React Hooks.

The above hook does a straightforward job managing the modal state. Let’s start by testing if it does not throw any errors.

Unfortunately, a test like the one above would not work. We can figure out the reason by reading the error message:

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

The React documentation confirms the above. We can only call hooks from function components, or other hooks. We could fix this issue using the enzyme library that we’ve covered in the previous part of this series and with a bit of cleverness:

Much better! The above solution is not very elegant, though, and does not provide us with a comfortable way to test our hook further. This is the reason for us to use the react-hooks-testing-library.

Introducing react-hooks-testing-library

The above provides us with utilities created solely for testing hooks. Its purpose is to mimic the experience of using hooks from within real components. Its renderHook function acts in a similar way to our   function that we’ve created before. It expects a callback that calls at least one hook.

Let’s install react-hooks-testing-library using  and write our first test:

The object returned by the renderHook function contains, among other things, the result. It two properties:

  • current – it reflects the return value of our hook
  • error – reflects the error thrown inside of a hook, if there was any

The  object also contains the   and   function. The documentation advises us to wrap functions, updating the state inside of the act utility. It simulates how the hooks work in a browser. Not using it results in a warning in the console.

Sometimes we need to pass arguments to the hook.

When doing the above, you might run into some corner cases. For a detailed explanation, please check the documentation.

Dealing with asynchronous hooks

There are sometimes situations in which hooks trigger asynchronous actions that the  object does not reflect at first. Let’s write a very simple hook that interacts with some API:

We want to test if successfully fetching comments causes the state to change. To wait for the   function to finish, we can use the waitForNextUpdate utility. It returns a promise that resolves next time the hook renders – typically due to an asynchronous update.

Our test might fail due to the API not working properly and we want to avoid that. Remember to mock the API first! If you want more details on how to do the above, check out, Mocking API calls and simulating React components interactions.

We can be more specific than just waiting for any update. With the use of the waitForValueToChange utility, we can wait for a particular value to change. To do the above, we provide a selector that returns the value that we want to wait for.

We also have the wait utility. The promise it returns resolves when the provided callback returns a truthy value, or undefined. We can pass additional options to all async utilities, such as the maximum time to wait. For a full list, check out the documentation.

Testing hooks with Redux

Our projects often use some state management, such as Redux or the context built into React. Let’s rewrite our  hook to use Redux.

A cool thing is that the return values for the   didn’t change. It shows how good React hooks are when it comes to refactoring.

Unfortunately, this time, the test fails. We can see the following error message:

Invariant Violation: could not find react-redux context value; please ensure the component is wrapped in a <Provider>

This is due to the fact that we didn’t so far provide any store for our hook to use. We can do it with the use of a second parameter of the renderHook function. When we pass the redux provider to the wrapper property of the options, the test component that uses our hook under the hood gets wrapped.

Please note that we render the children so that our hook can execute.

Summary

We’ve identified what the difficult parts of testing hooks are. Because we can only call them inside function components or other hooks, we need some utilities to test them. Instead of creating them ourselves, we can use the react-hooks-testing-library. In this article, we’ve learned how to test our hooks in more advanced cases, such as cases with asynchronous calls and Redux.

The react-hooks-testing-library is excellent for testing complex hooks. Also, it is very fitting for testing hooks that are highly reusable and not tied to a specific component. In other cases, when a hook is used in just one component, the creators of the react-hooks-testing-library express that it might be a better idea to test the component itself.

Series Navigation<< JavaScript testing #4. Mocking API calls and simulating React components interactionsJavaScript testing #6. Introduction to End-to-End testing with Cypress >>
avatar
  Subscribe  
newest oldest most voted
Notify of
Daniel

Is testing using @testing-library still unit testing in your opinion? Please argue your answer.