React jest hook testing: waitFor internal async code

Viewed 38

I have the following hook:

import { useEffect, useRef, useState } from "react";

function useAsyncExample() {
  const isMountedRef = useRef(false);
  const [hasFetchedGoogle, setHasFetchedGoogle] = useState(false);

  useEffect(() => {
    if (!isMountedRef.current) {
      isMountedRef.current = true;
      const asyncWrapper = async () => {
        await fetch("https://google.com");
        setHasFetchedGoogle(true);
      };
      asyncWrapper();
    }
  }, []);

  return hasFetchedGoogle;
}

With the following jest test (using msw and react-hooks testing library):

import { act, renderHook } from "@testing-library/react-hooks";
import { rest } from "msw";
import mswServer from "mswServer";
import useAsyncExample from "./useAsyncExample";

jest.useFakeTimers();

describe("using async hook", () => {
  beforeEach(() =>
    mswServer.use(
      rest.get("https://google.com/", (req, res, ctx) => {
        return res(ctx.json({ success: ":)" }));
      })
    )
  );

  test("should should return true", async () => {
    const { result, waitFor, waitForNextUpdate, waitForValueToChange } = renderHook(() => useAsyncExample());

    // ... things I tried
  });
});

And I am simply trying to wait for the setHasFetchedGoogle call.

I tried multiple things:

await waitForNextUpdate(); // failed: exceeded timeout of max 5000 ms

await waitForValueToChange(() => result.current[1]); // failed: exceeded timeout of max 5000 ms

await waitFor(() => result.current[1])  // failed: exceeded timeout of max 5000 ms

The closest I have come so far is the with the following:

const spy = jest.spyOn(global, "fetch");
// ...
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect(spy).toHaveBeenLastCalledWith("https://google.com");

But even this ends right before the setHasFetchedGoogle call happens, since it only await the fetch.

Online I found plenty of examples for component, where you can waitFor an element or text to appear. But this is not possible with hooks, since I am not rendering any DOM elements.

How can I listen to internal async logic of my hook? I though the waitForNextUpdate has exactly that purpose, but it doesn't work for me.

Any help is appreciated!

2 Answers

Actually it turns out my example works as I wanted it to.

The problem is that in the real-world case I have, the custom hook is more complex and has other logic inside which uses setTimeouts. Therefore I had jest.useFakeTimers enabled.

Apparently jest.useFakeTimers doesn't work together with waitForNextUpdate.

See more info

I will now try to get my tests to work by enabling/disabling the fakeTimers when I need them

As you said in your answer, you are using jest.useFakeTimers, but you are incorrect to say it doesn't work with waitForNextUpdate because it does.

Here is an example. I've modified your request to google to simply be an asynchronous event by waiting for two seconds. Everything should be the same with an actual mocked request though.

const wait = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay))

function useAsyncExample() {
  const isMountedRef = useRef(false);
  const [hasFetchedGoogle, setHasFetchedGoogle] = useState(false);

  useEffect(() => {
    if (!isMountedRef.current) {
      isMountedRef.current = true;
      const asyncWrapper = async () => {
        await wait(200);
        setHasFetchedGoogle(true);
      };
      asyncWrapper();
    }
  }, []);

  return hasFetchedGoogle;
}

// The test, which assumes a call to jest.useFakeTimers occurred in some beforeEach.
it('should should return true', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useAsyncExample())

  expect(result.current).toBe(false)
  act(() => {
    jest.advanceTimersByTime(200)
  })
  await waitForNextUpdate()
  expect(result.current).toBe(true)
})
Related