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?
- How to build a React app using Hooks
- How to test React Hooks
- Testing async Hook functions
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
anddecrement
buttons work - Submitting a name via the input field changes the value of the
name
state - The
name
state is saved in thelocalStorage
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:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. 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.
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.
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.