Testing a Custom React Hook (useMap)
Developers of the most popular React Hooks libraries rely on tests to enforce the overall quality of their libraries' code. Tests cover a wide variety of different use cases, give developers confidence that everything works as intended and serve as a form of documentation. Anytime a test fails, developers know which set of arguments to use to replicate the bug encountered and squash it lands in the distribution package. When writing a custom React Hook for a library, the Hook should be tested regularly and against the fringest of edge cases to appeal to a greater number of projects. Setting up a testing environment that executes tests fast and reliably requires the proper tools: This testing environment allows you to write tests that closely resemble your user's actions. For example, @testing-library/react comes with methods for rendering a React component ( render ) to a container ( document.body by default) and finding an element within the rendered content of this container via the element's label ( screen.getByLabelText ): With just these two methods, our test mimics, at a high-level, the browser rendering a contact form to the screen and the user searching for an e-mail address input field. In the case of React Hooks, you may not want to create dummy components for the sole purpose of testing your Hooks. A unit test for a React Hook should only test the Hook's functionality (independent of any component calling it). We should reserve the testing of Hooks called within a component for integration tests. Fortunately, the @testing-library/react-hooks library gives us testing utilities for testing Hooks in isolation without having to render any dummy components. Below, I'm going to show you how to test a custom React Hook built for a React Hooks library with the @testing-library/react-hooks library and Jest. To get started, clone this React Hooks library template from GitHub: This template has ESLint, TypeScript and Jest already configured and comes with a custom React Hook, useMap , which we will write tests for. Additionally, the @testing-library/react-hooks library has already been installed as a dev. dependency. The useMap Hook wraps around a Map object and mimics its API. If you would like to learn how to implement this Hook from scratch, then check out the blog post here . Within the __tests__ directory, create a new file, useMap.test.ts . We will write unit tests for the useMap Hook within this file. Within this file, import two methods from the @testing-library/react-hooks library: Then, import the useMap Hook. ( __tests__/useMap.test.ts ) Add a describe block with the text "useMap" to group all tests related to the useMap Hook within this one describe block. ( __tests__/useMap.test.ts ) Alongside the map state variable, which represents the Hook's current Map object, the useMap Hook provides several action methods for updating this state variable: From this point on, all describe blocks and tests will be written within the useMap describe block. Let's write a describe block for the set action method that covers two cases involving this action method: ( __tests__/useMap.test.ts ) For the "should update an existing key-value pair," we should render the useMap Hook using the renderHook utility method from the @testing-library/react-hooks library: ( __tests__/useMap.test.ts ) Here, we call the Hook with an array of one key-value pair. We pass this to a new Map object to create the initial map . The renderHook method returns an object with a result field. This field is a React ref. By reading the ref's current field, you can access the Hook's API (an array that contains the map state variable and the action methods). For now, let's omit the map state variable. I will explain why later on. ( __tests__/useMap.test.ts ) Let's double-check that our initial state was set correctly to a Map object with the key-value pair 1: "default" . ( __tests__/useMap.test.ts ) Save the changes. In the terminal, run the test npm script to verify that the initial state is set correctly: Now, let's call the set action to update the key-value pair of 1: default to 1: changed . We need to call the set action within the act method to flush any changes to the state into the simulated DOM before running any subsequent assertions. ( __tests__/useMap.test.ts ) Check that the key-value pair has been updated. The 1 key should have a corresponding value of "changed" . ( __tests__/useMap.test.ts ) Once again, save the changes. Run the test npm script to verify that the state has correctly changed: ( __tests__/useMap.test.ts ) With these few steps, you can write tests for the remaining action methods: ( __tests__/useMap.test.ts ) Remember how we omitted the map state variable and only accessed it from result.current[0] ? This is because all action methods are immutable. Therefore, anytime we call any one of these action methods, the map state variable destructured from the initial result.current will reference the initial Map object it's set to, not the new Map object that set it to internally in the Hook. Essentially, after the action method call, result.current references a completely different Map object than the one referenced by the destructured map state variable. Let's add a new describe block labeled "hook optimizations." Inside of this describe block, write a test to confirm this behavior: ( __tests__/useMap.test.ts ) Finally, let's write a test to make sure our useCallback and useMemo optimizations maintain reference equality after state changes. The reference to the action methods should never change. ( __tests__/useMap.test.ts ) Run the tests one final time and watch them all pass! Altogether... ( __tests__/useMap.test.ts ) For a final version of this code, visit the GitHub repository here . Try testing your own custom React Hooks with the @testing-library/react-hooks library.