Write Unit Tests for React Hooks using react-hooks-testing-library

Write Unit Tests for React Hooks using react-hooks-testing-library

Write Unit Tests for React Hooks using react-hooks-testing-library

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.

  1. Create a simple React project using Create-React-App.
npx create-react-app hooks-testing
  1. 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).

  1. Fetch data from the API using the fetch method and create a file named api.js in the src/ 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")
    }
}
  1. Create a Hook file used to fetch Post data from the API and save it in the src/ directory with the name useFetchPost.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.

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

YarnCommandResult

Additionally, you can debug to see what result from renderHook provides us.

RenderHook

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.

CustomHookTest

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 🙏 🙏 🙏

Related Posts

Let's check our site's accessibility easily using 'Accessibility Insights for Web' on Microsoft Edge.

Let's check our site's accessibility easily using 'Accessibility Insights for Web' on Microsoft Edge.

Hello to all the readers who have come across this article. Lately, I've been quite busy and it's taken me a while to find some free time to write on Medium. Today, I want to share some knowledge tha

read more
Awaitable Long Running Producer-Consumer

Awaitable Long Running Producer-Consumer

Normally we use producer-consumer problem to solve certain problems like write buffer, cache buffer or any operation (producer) that needs to entry data into a queue and having another operation (con

read more
Let's create our own Crypto coin, easy and in just a few minutes (no coding knowledge).

Let's create our own Crypto coin, easy and in just a few minutes (no coding knowledge).

Hello everyone who stumbled upon and came to read. I've been away from writing a blog for a while. Caught up with work, trying this and that, blah blah blah. But now it's time to come back and write

read more
Let's try to create Power Automate connected to SQL Server for sending Leave Approval Email using Low code together.

Let's try to create Power Automate connected to SQL Server for sending Leave Approval Email using Low code together.

Hello everyone! I've been away for a long time from blogging due to various events that have kept me busy, making it challenging to find time for writing. In my latest article, I briefly introduced P

read more
Customize the website to display using Tampermonkey

Customize the website to display using Tampermonkey

Many people may feel dissatisfied with certain websites when they browse them, for example:* Disliking intrusive banner advertisements that strain the eyes. * Wishing a website had specific feature

read more
Conditional Formatting (Fx) in PowerBI Custom Visual

Conditional Formatting (Fx) in PowerBI Custom Visual

👋 Hi Everyone 👋 During this time, I have a chance to implement the PowerBI Custom Visual with my team. And we spent a lot of time researching the conditional formatting (Fx) and we found many inter

read more
An Introduction to Microsoft's Power Automate

An Introduction to Microsoft's Power Automate

Today, we're introducing Microsoft's Power Automate, formerly known as Microsoft Flow. If you're familiar with Microsoft Power Platform services such as Dynamics 365, SharePoint, Power Apps, PowerBI,

read more
Pass Through Data Over IServiceProvider.CreateScope()

Pass Through Data Over IServiceProvider.CreateScope()

[ASP.NET] In some cases you may encounter the situation that you need to pass through some particular data over a new scope of Service Provider.For instance, when you implement a solution that inte

read more
Scan code with Credential Scanner on Azure DevOps

Scan code with Credential Scanner on Azure DevOps

🥳 Happy New Year 2023! 🥳Wishing everyone a great year ahead!Now, let's get down to business. Today, I'm going to introduce you to a handy tool that checks for leaked passwords, secrets, certifi

read more
Easy way to check user’s permission on SharePoint Online site in the web part

Easy way to check user’s permission on SharePoint Online site in the web part

Hello Everyone! 🎉 Happy New Year 2021 🎉 I hope you have a wonderful holiday, good vibes, and a nice party 🍻. This blog is the 2nd content which’s I write in English. In the previous blog, I explai

read more
SharePoint Group & Permission levels in SharePoint Online

SharePoint Group & Permission levels in SharePoint Online

Hello everyone 👋 !!! This is the 1st time that I wrote the SharePoint Online blog in English. This blog explains permission levels in SharePoint Online. Once you create a new site in SharePoint Onli

read more
Speed up and make your SPFx reloads quick and easy with the SPFx-Fast-Serve tool.

Speed up and make your SPFx reloads quick and easy with the SPFx-Fast-Serve tool.

Hello everyone! I'm back! In the past period, I've been busy expanding my knowledge, reading some Microsoft Learning materials, trying out new things, and handling various tasks, which left me with l

read more
Convert interface to enum (for those too lazy to type 'name' in the input form) in TypeScript

Convert interface to enum (for those too lazy to type 'name' in the input form) in TypeScript

![Convert interface to enum cover](/images/posts/transform-interface-as-enum-typescript/transform_interface-as-enum-cover.png)It's a small trick to convert an Interface to an Enum, helping to solve

read more
Utilize WebAssembly in .NET

Utilize WebAssembly in .NET

We heard the WebAssembly quite a while ago but the use case, especially for .NET developers, was still limited. As of the time writing this post, in the last quarter of 2022, there are many new thing

read more
What is SharePoint? How does it work? Let's take a look together! 😆

What is SharePoint? How does it work? Let's take a look together! 😆

Hello everyone who stumbled upon and is reading this content. After spending a considerable time exploring various content on Medium, I wanted to share a little bit of my knowledge. Having delved int

read more