Write Unit Tests for React Hooks using react-hooks-testing-library
- Nook Ittipon
- Tools , /thcategories/tools Programming , /thcategories/programming Web Programming /thcategories/web-programming
- 22 Mar, 2020
Hooks in React are a feature that has drastically changed the way we write React. It’s like undergoing plastic surgery in Korea, where some developers love the new look, while others prefer the old one. However, it’s not that extreme; we can still write React in the traditional way.
Now, let’s get to the point. For all the React devs who adore Hooks, today I’ll attempt to squeeze in some Unit Tests for the Hooks we’ve written ^0^ The hero of this story is the react-hooks-testing-library from https://react-hooks-testing-library.com/
Getting Started
In this article, the main focus will be on Unit Tests. Therefore, I will create a simple project using CRA (Create React App) because CRA helps us with the installation and configuration of the Test Runner, Jest, to a sufficient extent.
- Create a simple React project using Create-React-App.
npx create-react-app hooks-testing
- Navigate to the project folder and install and its dependencies (you can use npm or any convenient package manager).
yarn add @testing-library/react-hooks react-test-renderer
Here’s an example
I will create a Custom Hook called useFetchPost to fetch data from https://jsonplaceholder.typicode.com/posts/1 (with real traffic, to save a lot of time, please use a Mock API).
- Fetch data from the API using the fetch method and create a file named
api.js
in thesrc/
directory.
export const getPost = async (id) => {
try {
const fetchResult = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!fetchResult.ok) {
throw new Error("server error")
}
const jsonResult = await fetchResult.json()
return jsonResult;
} catch (error) {
throw new Error("something whet wrong")
}
}
- Create a Hook file used to fetch Post data from the API and save it in the
src/
directory with the nameuseFetchPost.js
.
import { useEffect, useState } from 'react';
import {getPost} from './api'
const useFetchPost = (id) => {
const [loading, setLoading] = useState(true)
const [post, setPost] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
const getPostFromApi = async () => {
try {
const result = await getPost(id)
setPost(result)
} catch(err) {
setError(err.message)
} finally {
setLoading(false)
}
}
getPostFromApi()
}, [id])
return [loading, post, error];
}
export default useFetchPost;
Let me explain a bit:
Lines 1-3: This custom hook uses useState and useEffect and imports getPost from ./api.
Lines 7-9: Declare the state variables that will be used. The states are as follows:
2.1 loading: Indicates that data is being fetched from the API.
2.2 post: Contains the data fetched from the API.
2.3 error: Displays an error message if there is an issue while fetching data.
Lines 11-24: Uses useEffect to fetch data based on the id provided to this custom hook and sets the states for 2.1-2.3.
Line 26: Returns all three states as an array in the order of loading, post, and error.
- Now let’s write unit tests for useFetchPost. First, let’s define the test cases. I will present two scenarios:
3.1 When successfully fetching Post data from the API using a specific id, it should return the Post data.
3.2 When encountering an issue during data fetching, it should not return Post data but instead return an error.
Let’s start creating the test file named useFetchPost.test.js
in the src/
directory. CRA is already configured to recognize files with *.test.js
as test files.
import { renderHook } from '@testing-library/react-hooks';
import useFetchPost from './useFetchPost';
import { getPost } from './api';
jest.mock('./api')
const mockGetPost = getPost;
describe('the useFetchPost hook', () => {
let mockFetchPostResult;
let mockPostObject;
beforeEach(() => {
mockPostObject = {
id: "1",
title: "mock title",
body: "mock body of post"
}
mockFetchPostResult = Promise.resolve(mockPostObject)
})
it('should return Post object result', async () => {
mockGetPost.mockImplementation(() => mockFetchPostResult)
const { result, waitForNextUpdate } = renderHook(() => useFetchPost(1))
await waitForNextUpdate()
const [loading, post, error] = result.current
expect(loading).toEqual(false)
expect(post).toEqual(mockPostObject)
expect(error).toEqual(null)
})
it('should return error message when getPost have problem', async () => {
const mockErrorMessage = new Error("something wrong")
mockGetPost.mockImplementation(() => Promise.reject(mockErrorMessage))
const { result, waitForNextUpdate } = renderHook(() => useFetchPost(1))
await waitForNextUpdate()
const [loading, post, error] = result.current
expect(loading).toEqual(false)
expect(post).toEqual(null)
expect(error).toEqual(mockErrorMessage.message)
})
})
Explanation:
Line 1: Import renderHook
from the testing library.
import { renderHook } from '@testing-library/react-hooks';
After that, import useFetchPost
to conduct the testing.
import useFetchPost from './useFetchPost';
Since unit tests focus on testing the individual unit only, without involving other parts, we simulate API calls using a short Mock API for useFetchPost
. We mock the getPost
function from the api.js
file using Jest, which comes with useful mocking tools provided by CRA. For more information, refer to https://jestjs.io/docs/en/mock-functions.
import { getPost } from './api';
jest.mock('./api')
const mockGetPost = getPost;
Name this test scope and define variables that can be used throughout this test scope.
describe('the useFetchPost hook', () => {
let mockFetchPostResult;
let mockPostObject;
Before running each test case using beforeEach
, set the initial values for mockPostObject
, which will return post data, and mockFetchPostResult
, which simulates the successful API data retrieval by mocking the getPost
function to return a resolved promise.
beforeEach(() => {
mockPostObject = {
id: "1",
title: "mock title",
body: "mock body of post"
}
mockFetchPostResult = Promise.resolve(mockPostObject)
})
Our first test case will verify that calling the getPost
API is successful and should return the Post data. To achieve this, we mock the getPost
function to return a resolved Promise (simulating a successful API call) that we’ve set up in the beforeEach
block.
it('should return Post object result', async () => {
mockGetPost.mockImplementation(() => mockFetchPostResult)
Still in the first test case, now comes the hero of our story, renderHook
. It helps us perform unit tests for Hooks. As the name suggests, it renders our useFetchPost
Hook. As for await waitForNextUpdate()
, it assists Hooks that work asynchronously. You can find more information about it here: https://react-hooks-testing-library.com/usage/advanced-hooks#async
const { result, waitForNextUpdate } = renderHook(() => useFetchPost(1))
await waitForNextUpdate()
Next, we perform an expect assertion on the object returned by useFetchPost
, which comprises [loading, post, error] (variable names can be anything, but the positions must match). This object is stored in result.current, where in the first test case, loading should be false, post should have the value of mockPostObject
, and error should be null (indicating no error occurred during the successful API retrieval).
const [loading, post, error] = result.current
expect(loading).toEqual(false)
expect(post).toEqual(mockPostObject)
expect(error).toEqual(null)
The next test case, the second one, tests when an error occurs. It should return an error and no post data. This case is similar to the first test case, but now we will mock the getPost function from the API to make it reject instead. During the expect assertion, we expect an error message to be returned and ensure that there is no post data sent back.
it('should return error message when getPost have problem', async () => {
const mockErrorMessage = new Error("something wrong")
mockGetPost.mockImplementation(() => Promise.reject(mockErrorMessage))
const { result, waitForNextUpdate } = renderHook(() => useFetchPost(1))
await waitForNextUpdate()
const [loading, post, error] = result.current
expect(loading).toEqual(false)
expect(post).toEqual(null)
expect(error).toEqual(mockErrorMessage.message)
})
Try running the tests (as before, you can use npm).
yarn test
Result
Additionally, you can debug to see what result from renderHook
provides us.
How to Debug Tests with VS Code: https://create-react-app.dev/docs/debugging-tests#debugging-tests-in-visual-studio-code
Once you have the tests, you can confidently use your custom Hooks.
Finally, it’s the end. For the example of writing Unit Tests for the Custom Hooks we created, this article is my first one. If there were any mistakes in the writing, I apologize here 🙏 🙏 🙏