Yomi Eluwande JavaScript developer. Wannabe designer and Chief Procrastinator at Selar.co and worklogs.co.

How to test React Hooks

7 min read 2110

react hooks testing enzyme jest

Editor’s note: This article was last updated on 30 June, 2022 to correct inconsistencies in the previous version and include more up-to-date information on testing React Hooks.

The 16.8.0 version release of React meant a stable release of the React Hooks feature. React Hooks was introduced in 2018 and got favorable reviews from the React ecosystem. It’s essentially a way to create components with features, like state, without the need for class components.

The importance of testing in the front end can’t be stressed enough, as every team and company employs test-driven development in order to instill confidence in their software. The goal of this article is to provide a practical guide on testing React Hooks using tools such as React Testing Library, Jest, and Enzyme.

Contents

What are React Hooks?

The Hooks feature is a welcome change as it solves many of the problems React devs have faced over the years. One of those problems is the case of React not having support for reusable state logic between class components. This can sometimes lead to huge components, duplicated logic in the constructor, and lifecycle methods.

Inevitably, this forces us to use some complex patterns such as render props and higher order components, and that can lead to complex codebases.

Hooks aim to solve all of these by enabling you to write reusable components with access to state, lifecycle methods, and refs.

How to build a React app using React Hooks

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.

The whole app can be seen and interacted with at 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.

The other Hook that’s in use here is the useEffect Hook. 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 lifecycles:

// 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 data gotten into a state.

How to test React Hooks

Testing React Hooks with Jest and Enzyme

Jest and Enzyme are tools used for testing React apps. Jest is a JavaScript testing framework used to test JavaScript apps, and Enzyme is a JavaScript testing utility for React that makes it easier to assert, manipulate, and traverse your React components’ output.

Let’s see how they can be used 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.

Testing the useState Hook with Enzyme

In order to 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.



Testing React Hooks with React Testing Library

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 is already installed in CRA and is the default test library for React.

Let’s see an example of writing tests for Hooks using React Testing Library.

In the app above, three types of Hooks are in use, useState, useEffect, and useRef, and 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, which means the input’s value can now be accessible through the ref.

The useEffect Hook implementation is essentially setting the value of the name state to the localStorage.

Let’s go ahead and write tests for all of the implementation above. We’ll be writing the test for the following:

  • The initial count state is zero
  • The increment and decrement buttons work
  • Submitting a name via the input field changes the value of the name state
  • The name 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";

render  will help render our component. It renders into a container which 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 button.

Next, 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 if the initial count state is set to zero by first fetching the element with the getByTestId helper. It then checks if the content is zero using the expect() and toBe() functions.


More great articles from LogRocket:


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 if the onButton is clicked, the state is set to one and when the offButton is clicked on, the state is set to zero.

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, and 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, the fireEvent.change method is used to enter a value into the input field, after which the submit button is clicked on.

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, a reload of the app is simulated and there’s a check to see if name set previously was stored to the localStorage.

Testing async Hook functions

To implement tests for asynchronous Hooks, we can use the waitForNextUpdate function from the React Hooks Testing Library.

The async methods return promises, so be sure to call them with await or .then. The React Hooks Testing Library provides a number of async methods for testing async Hooks, which include:

  • waitFor
  • waitForValueToChange
  • waitForNextUpdate

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 React from 'react';
import axios from 'axios';

export default (endpoint) => {
  const [data, setData] = React.useState({
    state: "",
    error: '',
    data: [],
  });

  const fetchData = () => {
    setData({
      state: "LOADING",
      error: '',
      data: [],
    });
    axios(endpoint)
      .then((resp) => resp.json())
      .then((respData) => {
        setData({
          ...data,
          state: "SUCCESS",
          data: respData,
        });
      })
      .catch((err) =>
        setData({
          ...data,
          state: "ERROR",
          error: 'Fetch failed',
        })
      );
  };

  React.useEffect(() => {
    fetchData();
  }, []);

  return data;
};

Now, let’s test our async Hook.

Install the testing library for React Hooks with the following command:

Yarn add @testing-library/react-hooks

Now, create a useFetchData.test.js file in the src folder and add the following:

import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import useFetchData from './useFetchData.js';
import axios from 'axios';

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 a typical response data for our test.

Next, we’ll write a test for successful API requests and failed API requests:

describe('useFetchData Hook', () => {
  it('initial and success state', async () => {
    axios.get.mockResolvedValue(useApiMockData);
    const { result, waitForNextUpdate } = renderHook(() =>
      useFetchData('lorem')
    );
    expect(result.current).toMatchObject({
      data: [],
      error: '',
      state: 'LOADING',
    });

    await waitForNextUpdate();

    expect(result.current).toMatchObject({
      data: useApiMockData,
      error: '',
      state: 'SUCCESS',
    });
  });

  it('error state', async () => {
    const errorMessage = 'Network Error';
    axios.get.mockImplementationOnce(() =>
      Promise.reject(new Error(errorMessage))
    );

    const { result, waitForNextUpdate } = renderHook(() =>
      useFetchData('lorem')
    );

    await waitForNextUpdate();

    expect(result.current).toMatchObject({
      data: [],
      error: 'Fetch failed',
      state: 'ERROR',
    });
  });
});

Notice the waitForNextUpdate(). It simulates a state update as a result of an asynchronous update and returns a promise that resolves the next time the Hook renders.

Conclusion

In this article, we’ve seen how to write tests for React Hooks and React components using the react-testing-library. We also went through a short primer on how to use React Hooks. 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.

Yomi Eluwande JavaScript developer. Wannabe designer and Chief Procrastinator at Selar.co and worklogs.co.

10 Replies to “How to test React Hooks”

  1. Its not about testing react hooks, but react component using hooks. :disappointed:

  2. 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?

  3. 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:

  4. 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?

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

  6. In the example with Testing async Hook functions I am getting the following error
    Timed out in waitForNextUpdate after 1000ms.

Leave a Reply