Search code examples
reactjsunit-testingreact-hooksjestjsreact-hooks-testing-library

testing-library/react-hooks calling a function that change the state


I tried everything but still for some reason the react state doesn't change, what is I am missing or what is the alternative approach? Code:

export const useCounter = (): UseCounterResult => {
  const session = useSession();
  const [list, setList] = useState<Customer[]>([]);
  const [counter, setCounter] = useState(0);
  const [limit, setLimit] = useState(10);
  useEffect(() => {
    (async () => {
      const response = await getApiClient(session).listCustomers();
      setList((response?.customers || []));
      setCounter(response.todayCount);
      setLimit(response.dailyLimit);
    })();
  }, [session]);

  function addToList(newItem: Customer) {
    setList((list) => [...newItem,...list]);
    setCounter((counter) => counter + 1);
  }

  return {counter, limit, list, addToList};
};

And the UT I tried but doesn't work:


  it.only("should add to list", async () => {
    // arrange
    (getApiClient as jest.Mock).mockReturnValue({
      listCustomers: async () => ({
        customers: [],
        todayCount: 1,
        dailyLimit: 5,
      }),
    });
    // act
    const { result, unmount, waitForNextUpdate, rerender } =
      renderHook(useCounter);
    act(() => {
      result.current.addToList({
        mobileNumber: "John",
        receiverName: "+61456789123",
      });
    });
    // await waitForNextUpdate();
    // assert
    await waitFor(() => expect(result.current.counter).toBe(2));
    unmount();
  });

I tried without wrapping into act and instead calling waitForNextUpdate after it, still didn't work, result.current.counter is 1 instead of 2.


Solution

  • Don't call addToList until after you know that your async API call inside your useEffect completes - because when that call completes it will overwrite the state that may have been set by an addToList call if the async call happened to be in progress.

    You can do that by asserting that result.current.dailyLimit contains the value returned from the API.

    After your test has asserted that, then go ahead and make your call to addToList and perform your subsequent assertion

      it.only("should add to list", async () => {
        // arrange
        (getApiClient as jest.Mock).mockReturnValue({
          listCustomers: async () => ({
            customers: [],
            todayCount: 1,
            dailyLimit: 5,
          }),
        });
        // act
        const { result, unmount, waitForNextUpdate, rerender } =
          renderHook(useCounter);
    
        // now you know your async call has resolved
        await waitFor(() => expect(result.current.dailyLimit).toEqual(5));
    
        // call add to list - you don't need to wrap it in `act` because you have `waitFor` right after
        result.current.addToList({
          mobileNumber: "John",
          receiverName: "+61456789123",
        });
    
        await waitFor(() => expect(result.current.counter).toBe(2));
        unmount();
      });