Search code examples
reactjsreact-hooksreact-testing-library

How do I test a custom hook that has async code?


The below is a simple custom hook that fetches data from a GET request and returns it.

import { useState, useEffect } from "react";

export const useData = (url) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data));
  }, [url]);

  return [data];
};

I am trying to test this custom hook but while testing the actual response I get is an array with a null element [null].

This is my test file.

import { renderHook, waitFor } from "@testing-library/react";
import { useData } from "./useData";

test("fetching the jsonarray", async () => {

  const { result } = renderHook(() =>
    useData("https://jsonplaceholder.typicode.com/todos?_limit=1")
  );

  let actualResponse;

  await waitFor(() => {
    actualResponse = result.current;
  });

  expect(actualResponse).toEqual([
    {
      userId: 1,
      id: 1,
      title: "delectus aut autem",
      completed: false,
    },
  ]);
});

I get the below. As you can see below, in my test case, the actual response is [null]. How do I make the the test case wait until the async fetch call is completed?

 Expected  - 6
    + Received  + 1

      Array [
    -   Object {
    -     "completed": false,
    -     "id": 1,
    -     "title": "delectus aut autem",
    -     "userId": 1,
    -   },
    +   null,
      ]

Solution

  • The waitFor function works as expected. You should use result.current[0], not result.current in the callback of the waitFor function since you return the data as [data].

    result.current is an array [], which is a truthy value, the waitFor function will not retry to call the callback.

    Returning a falsy condition is not sufficient to trigger a retry, the callback must throw an error in order to retry the condition

    That's why we should use expect(result.current[0]).toBeTruthy(), this assertion may throw an error to trigger a retry when result.current[0] is null.

    The title of the todo data may be random, that's why I use expect.any(String).

    import { renderHook, waitFor } from '@testing-library/react';
    import { useData } from './useData';
    
    test('fetching the jsonarray', async () => {
      const { result } = renderHook(() => useData('https://jsonplaceholder.typicode.com/todos?_limit=1'));
    
      await waitFor(() => expect(result.current[0]).toBeTruthy());
    
      expect(result.current[0]).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            userId: 1,
            id: 1,
            title: expect.any(String),
            completed: false,
          }),
        ]),
      );
    });
    

    Test result:

     PASS  stackoverflow/77863992/useData.test.ts
      √ fetching the jsonarray (957 ms)
                                                                                                                                                                                                                                                                   
    Test Suites: 1 passed, 1 total                                                                                                                                                                                                                                 
    Tests:       1 passed, 1 total                                                                                                                                                                                                                                 
    Snapshots:   0 total
    Time:        1.829 s, estimated 2 s
    Ran all test suites related to changed files.
    

    package versions:

    "@testing-library/react": "^14.1.2",
    "react": "^18.2.0",
    "jest": "^29.7.0",
    

    jest.setup.js:

    import 'whatwg-fetch';
    

    jest.config.js:

    module.exports = {
      testEnvironment: 'jsdom',
      setupFiles: ['<rootDir>/jest.setup.js'],
    };