Editor’s note: This article was last updated by Isaac Okoro on 18 December 2023 to add a more in-depth overview and comparison of Jest, Enzyme, and React Testing Library, discuss solutions to common challenges encountered while testing React Hooks, and provide updated information related to the most recent React v18.
The stable release of React Hooks in React v16.8.0 was met with favorable reviews from the community. Along with this stable release came the need to test React Hooks efficiently.
The importance of testing in the frontend can’t be stressed enough. Every team and company employs test-driven development to instill confidence in their software. This article provides a practical guide to testing React Hooks using tools such as React Testing Library, Jest, and Enzyme.
React Hooks essentially provide a way to create components with features, like state, without the need for class components.
Standard and custom React Hooks solve many of the problems React devs faced over the years. These problems inevitably forced us to use some complex patterns as workarounds, such as render props and higher-order components, which can lead to complex codebases.
For example, before introducing Hooks, React had no support for reusable state logic between class
components. This sometimes led to huge components, duplicated logic in the constructor, and the need to use lifecycle methods.
Hooks aim to solve all of these by enabling you to write reusable components with access to state, lifecycle methods, and refs.
Before we go on to see how to write tests for React Hooks, let’s see how to build a React app using Hooks. We’ll be building an app that shows the 2018 F1 races and the winners for each year.
You can see and interact with the whole app via CodeSandbox:
In the app above, we’re using the useState
and useEffect
Hooks. If you navigate to the index.js
file, in the App
function, you’ll see an instance where useState
is used:
// Set the list of races to an empty array let [races, setRaces] = useState([]); // Set the winner for a particular year let [winner, setWinner] = useState(); useState returns a pair of values, that is the current state value and a function that lets you update it. It can be initialized with any type of value (string, array e.t.c) as opposed to state in classes where it had to be an object.
Meanwhile, the useEffect
Hook adds the ability to perform side effects from a function component. It essentially allows you to perform operations you’d usually carry out in the componentDidMount
, componentDidUpdate
, and componentWillUnmount
lifecycle methods:
// On initial render of component, fetch data from API. useEffect(() => { fetch(`https://ergast.com/api/f1/2018/results/1.json`) .then(response => response.json()) .then(data => { setRaces(data.MRData.RaceTable.Races); }); fetch(`https://ergast.com/api/f1/2018/driverStandings.json`) .then(response => response.json()) .then(data => { let raceWinner = data.MRData.StandingsTable.StandingsLists[0].DriverStandings[0].Driver.familyName + " " + data.MRData.StandingsTable.StandingsLists[0].DriverStandings[0].Driver.givenName; setWinner(raceWinner); }); }, []);
In the app, we’re using the useEffect
Hook to make API calls and fetch the F1 race data, then using the setRaces
and setWinner
functions to set their respective values into the state.
That’s just an example of how Hooks can be used in combination to build an app. We use the useEffect
Hook to fetch data from some source and the useState
to set the fetched data into a state.
Jest is a JavaScript testing framework with a focus on simplicity that allows you to write tests for your JavaScript applications easily. It also works for testing TypeScript, React, React Native, and Vue applications.
Jest comes with an out-of-the-box configuration for JavaScript files and an elegant API, which is used to create isolated tests, snapshot comparisons, test mocking, test coverage, and much more.
React Testing Library and Enzyme are both testing utilities that provide methods for allowing you to access DOM elements. However, they don’t handle assertions, which check whether a certain condition is true and throw an error if it’s not.
To handle assertions, you need a test runner and framework. That’s where Jest comes in. Jest acts like a test runner that identifies tests, executes them, and decides whether they passed or failed. It also provides functions for assertions, code implementation, and test suites.
Jest is considered the de facto testing framework for React applications, making it a popular choice within the React ecosystem. Its ease of use, extensive features, and active community contribute to its widespread adoption for testing.
Enzyme is a JavaScript testing utility for React that makes it easier to assert, manipulate, and traverse the output of your React components. It also provides a set of convenient methods to work with React components, enabling developers to write tests for their React applications more effectively.
Some notable features of Enzyme include:
Let’s see how we can use Enzyme to test React Hooks.
To start, let’s create a project using Create React App as follows:
npx create-react-app my-app cd my-app
Next, we’ll install the Enzyme test library along with a React adapter as follows:
npm i --save-dev enzyme enzyme-adapter-react-16
Now, create a file called setupTests.js
in the src
folder. Add the following below snippet to configure Enzyme’s adapter:
import Enzyme from "enzyme"; import Adapter from "enzyme-adapter-react-16"; Enzyme.configure({ adapter: new Adapter() });
The code in the setupTests.js
file gets executed before our test is executed.
useState
Hook with EnzymeTo test the useState
Hook, let’s update the app.js
file with the following:
import React from "react"; const App= () => { const [name, setName] = React.useState(""); return ( <form> <div className="row"> <div className="col-md-6"> <input type="text" placeholder="Enter your name" className="input" onChange={(e) => { setName(e.target.value); }} /> </div> </div> <div className="row"> <div className="col-md-6"> <button type="submit" className="btn btn-primary" > Add Name </button> </div> </div> </form> ); }; export default App;
Here, we have a basic input field and a button element. Notice how we used React.useState()
instead of useState()
. We need this to support mocking the useState
Hook in our Enzyme test:
import React from "react"; import { shallow } from "enzyme"; import App from "./App"; const setState = jest.fn(); const useStateSpy = jest.spyOn(React, "useState"); useStateSpy.mockImplementation((initialState) => [initialState, setState]); const wrapper = shallow(<App />);
Here, we’ve successfully mocked the useState
Hook and we can proceed with testing for the state update on input change as follows:
it("should update state on input change", () => { const newInputValue = "React is Awesome"; wrapper .find(".input") .simulate("change", { target: { value: newInputValue } }); expect(setState).toHaveBeenCalledWith(newInputValue); });
Enzyme supports React Hooks, although there are some downsides in .shallow()
due to upstream issues in React’s shallow renderer. With React shallow renderer, useEffect()
and useLayoutEffect()
don’t get called.
React Testing Library is a lightweight solution for testing React components. It extends upon react-dom
and react-dom/test-utils
to provide light utility functions. It encourages you to write tests that closely resemble how your React components are used.
React Testing Library’s main goal is to boost developers’ confidence in their tests by testing components in the way a user would use them. It’s already installed in CRA, making it a readily available — and therefore commonly used — test library for React.
Let’s see an example of writing tests for Hooks using React Testing Library.
In the app above, we’re using three types of Hooks — useState
, useEffect
, and useRef
. We’ll be writing tests for all of them.
For the useRef
Hook implementation, we’re essentially creating a ref
instance using useRef
and setting it to an input field. This means we can now access the input’s value through the ref. Meanwhile, the useEffect
Hook test essentially sets the value of the name
state to the localStorage
.
Let’s go ahead and write tests for all of the implementations above. We’ll be writing tests to check that:
count
state is 0
increment
and decrement
buttons workname
statename
state is saved in the localStorage
Navigate to the __tests__
folder to see the hooktest.js
file that contains the test suite and the import
line of code below:
// hooktest.js import { render, fireEvent, getByTestId} from "react-testing-library";
In this line of code:
render
  will help render our component. It renders into a container that is appended to document.body
getByTestId
  fetches a DOM element by data-TestId
fireEvent
is used to “fire” DOM events. It attaches an event handler on the document and handles some DOM events via event delegation — e.g., clicking a buttonNext, add the test suite below in the hooktest.js
file:
// hooktest.js it("App loads with initial state of 0", () => { const { container } = render(<App />); const countValue = getByTestId(container, "countvalue"); expect(countValue.textContent).toBe("0"); });
The test checks that the initial count state is set to 0
by first fetching the element with the getByTestId
helper. It then checks if the content is 0
using the expect()
and toBe()
functions.
Next, we’ll write the test to see if the increment and decrement buttons work:
// hooktest.js it("Increment and decrement buttons work", () => { const { container } = render(<App />); const countValue = getByTestId(container, "countvalue"); const increment = getByTestId(container, "incrementButton"); const decrement = getByTestId(container, "decrementButton"); expect(countValue.textContent).toBe("0"); fireEvent.click(increment); expect(countValue.textContent).toBe("1"); fireEvent.click(decrement); expect(countValue.textContent).toBe("0"); });
In the test above, we are checking that the state is set to 1
if the onButton
is clicked and that the state is set to 0
when the offButton
is clicked.
For the next step, we’ll write a test to assert if submitting a name via the input field actually changes the value of the name
state, as well as to check that it’s successfully saved to the localStorage
:
// hooktest.js it("Submitting a name via the input field changes the name state value", () => { const { container, rerender } = render(<App />); const nameValue = getByTestId(container, "namevalue"); const inputName = getByTestId(container, "inputName"); const submitButton = getByTestId(container, "submitRefButton"); const newName = "Ben"; fireEvent.change(inputName, { target: { value: newName } }); fireEvent.click(submitButton); expect(nameValue.textContent).toEqual(newName); rerender(<App />); expect(window.localStorage.getItem("name")).toBe(newName); });
In the test assertion above, we used the fireEvent.change
method to enter a value into the input
field, after which we fired an event to click on the submitButton
.
The test then checks if the value of the ref after the button was clicked is equal to the newName
. Finally, using the rerender
method, we simulate a reload of the app and check to see if the name set previously was stored to the localStorage
.
To implement tests for asynchronous Hooks, we can use the waitForNextUpdate
function from the React Hooks Testing Library.
Async methods return promises, so be sure to call them with await
or .then
. The React Hooks Testing Library provides the waitFor
method for testing async Hooks.
The async Hook that we’ll test accepts an API URL as a parameter, makes an asynchronous request with Axios, and returns a response object.
Create a useFetchData.js
file in the src
folder and add the following:
import axios from "axios" const endpoint = "https://jsonplaceholder.typicode.com/posts/1" export default function Endpoint() { const [data, setData] = React.useState({ state: "LOADING", error: "", data: [] }) const fetchData = async () => { try { const result = await axios.get(endpoint) setData({ state: "SUCCESS", error: "", data: result.data // Access the data directly }) } catch (err) { setData({ data: [], state: "ERROR", error: err }) } } React.useEffect(() => { fetchData() }, []) return data }
Now, let’s test our async Hook. Create a useFetchData.test.js
file in the src
folder and add the following:
import { renderHook } from "@testing-library/react" import useFetchData from "./useFetchData" import axios from "axios" import { act } from "react-dom/test-utils" jest.mock('axios'); const useApiMockData = [ { id: 1, name: "Leanne Graham" }, { id: 2, name: "Ervin Howell" } ]
Here, we use a jest.mock
method to mock Axios for testing async actions. Also, we’ve mocked some typical response data for our test.
Next, we’ll write a test for successful and failed API requests:
describe("useFetchData Hook", () => { it("initial and success state", async () => { axios.get.mockResolvedValue(useApiMockData) const { result } = renderHook(() => useFetchData()) act(() => { result.current.state = "SUCCESS" result.current.error = "" result.current.data = [] }) expect(result.current).toMatchObject({ data: [], error: "", state: "SUCCESS" }) }) it("error state", async () => { const errorMessage = "Network Error" axios.get.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)) ) const { result } = renderHook(() => useFetchData()) act(() => { result.current.state = "ERROR" result.current.error = "Fetch failed" result.current.data = [] }) expect(result.current).toMatchObject({ data: [], error: "Fetch failed", state: "ERROR" }) }) })
In the code block above, we are mocking our Axios call and the state updates. Notice how the act()
method wraps code that causes any state updates as a result of an asynchronous request. Now, when we run our test, we see that everything passes as shown in the image below:
Testing React Hooks can be quite challenging. This section will cover some common pitfalls you may encounter and solutions for dealing with them.
There are certain scenarios in your React application when your React Hook may depend on certain operations, packages, or APIs. This can be difficult to isolate and test. If these dependencies are external APIs that can change at any time, simulating the behavior is even more challenging.
The solution to this challenge is to mock your dependencies. Mocking involves substituting real components or functions with simulated versions when running tests.
Below is an example of how to mock an API request:
import axios from 'axios'; jest.mock('axios'); const mockedData = [{ "id": 1, "name": "Jimmy", "isVerified": true, }, { "id": 2, "name": "Tommy", "isVerified": false, }]; describe('Mocking an API', () => { test('should fetch data from API, async () => { axios.get.mockResolvedValue({ data: mockedData }); }); })
In the code block above, we imported the dependency our app will be using — in this case axios
. Then, we mocked the dependency with Jest and faked the expected result
Side effects are any behaviors that occur outside the scope of the function being executed. An example of a side effect is data fetching or browser DOM manipulation. Testing side effects can be tricky because they are mostly asynchronous in nature, which can lead to unpredictable test outcomes.
The solution to this is to use the waitFor
function from React Testing Library with async/await
to wait for asynchronous operations to complete before making assertions:
import { waitFor } from '@testing-library/react'; test('should trigger side effect', async () => { // Trigger the side effect const resultPromise = someAsyncFunction(); // Wait for the promise to resolve await waitFor(() => { expect(resultPromise).resolves.toBe(/* expected value */); }); });
The code block above shows how to use the waitFor
function with async/await
to wait for asynchronous code.
Testing custom Hooks in React can also be tricky because custom Hooks are different from components. You get an error when you try to test them using the render()
function from React Testing Library, which stems from the fact that React Hooks cannot be used outside of React components.
The solution for this is to use the renderHook()
function instead of the render()
function. The renderHook()
function provides an environment to render Hooks outside of components as shown in the code block below:
import { renderHook } from '@testing-library/react'; import useCustomHook from './useCustomHook'; test('should do something', () => { const { result } = renderHook(() => useCustomHook()); // Assertions based on the hook's return value expect(result.current.someValue).toBe(/* expected value */); });
In the code above, we imported the renderHook()
function and the custom Hook. Then, we successfully carried out our tests without errors.
Let’s compare Enzyme, Jest, and React Testing Library using metrics such as performance testing, mocking capabilities, scalability, and more:
Metrics | Enzyme | Jest | React Testing Library |
---|---|---|---|
Performance | Performs well, but its extensive set of features can impact performance. Certain factors, like project size, may also limit performance | Highly performant | Known for its lightweight design, which contributes to better performance in rendering and querying |
Usability | Provides rich features for component-centric testing and manipulation | General-purpose testing framework that offers a comprehensive set of features | Emphasizes simplicity, user-centric testing, and efficient querying of components |
Snapshot testing | Supported via the enzyme-to-json package | Supported | Can handle snapshot testing but it wasn’t designed for that |
Mocking capabilities | Powerful mocking capabilities and utilities | Comes with built-in mocking capabilities | Limited built-in mocking |
Updates and maintenance | Actively maintained | Actively maintained | Actively maintained |
Scalability | Becomes less scalable for large applications | Scales well for applications of any size | Scales well for applications of any size |
This table should give you a better idea of when and how to use each tool. Remember, as we discussed before, Jest is a great tool to use together with Enzyme or React Testing Library. For example, you could use Jest to handle assertions while using React Testing Library to manage the interaction with React components.
In this article, we reviewed how to use React Hooks and discussed how to write tests for React Hooks and React components using Jest, Enzyme, and React Testing Library. We also compared these three options for testing React components to better understand how each tool could fit into our testing strategy.
Additionally, we covered some of the challenges of testing React Hooks and solutions you can implement to deal with these challenges. You can check out the demo project we used to explore testing React Hooks on CodeSandbox.
Want to learn more? Check out this post on how to avoid common mistakes with React Hooks. If you have any questions or comments, you can share them below.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
10 Replies to "How to test React Hooks"
Unfortunately, the tests at https://codesandbox.io/s/rqj0lymyn do not run due to
`Invariant Violation: Target container is not a DOM element.`
Its not about testing react hooks, but react component using hooks. :disappointed:
I was able to test a react component using hooks with enzyme.
good article, concise summary on the basics & tooling for react hook-component testing. thanks! how do you stub HTTP requests? for example, is there a way to intercept the fetch requests going into the first Formula 1 app on mount use effect hook?
Thank you for the aha moment!
It is just for testing simple react hooks function that you can get from anywhere. You haven’t added Testing for async hook component. :disappointed:
Good read! đź‘Ť
Nice Article! Can you please tell if there is any way to override the default value of the useState(0). For example if I initially want to render the counter using the value 1 instead of 0?
This article and the react addition of hooks makes me want to throw up. If you add state to a pure function, *that function is no longer pure*!! If you add effects to the function, *it is not a pure function!!!*
The reason this is hard to test is *because* the functions are no longer pure once you do this! There’s a huge amount of context that has to exist for that function to work, none of which is a passed in argument! You can’t have your cake and eat it too.
In the example with Testing async Hook functions I am getting the following error
Timed out in waitForNextUpdate after 1000ms.