Search code examples
reactjsreact-hookstesting-library

Testing custom hooks with Testing Library/React-Hooks


I have more of a conceptual question regarding testing-library/react-hooks.

I have the following test:

  describe('something else', () => {
    const mock = jest.fn().mockResolvedValue({ data: [['NL', 'Netherlands'], ['CU', 'Cuba']], status: 200 });
    axios.get = mock;

    it('calls the api once', async () => {
      const setItemMock = jest.fn();
      const getItemMock = jest.fn();
      global.sessionStorage = jest.fn();
      global.sessionStorage.setItem = setItemMock;
      global.sessionStorage.getItem = getItemMock;

      const { waitFor } = renderHook(() => useCountries());

      await waitFor(() => expect(setItemMock).toHaveBeenCalledTimes(0));
    });
  });

Which test the following custom hook:

import { useEffect, useState } from 'react';
import axios from '../../shared/utils/axiosDefault';
import { locale } from '../../../config/locales/locale';

type TUseCountriesReturnProps = {
  countries: [string, string][];
  loading: boolean;
  error: string;
}

export default function useCountries(): TUseCountriesReturnProps {
  const [countries, setCountries] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const sessionStorageCountriesKey = `countries-${locale}`;

  useEffect(() => {
    const countriesFromStorage = sessionStorage.getItem(sessionStorageCountriesKey);

    const getCountries = async () => {
      try {
        const response = await axios.get('/api/v3/countries', {
          params: {
            locale,
          },
        });
        console.log(response);

        if (response.status === 200) {
          setCountries(response.data);
          sessionStorage.setItem(sessionStorageCountriesKey, JSON.stringify(response.data));
        } else {
          console.error(response);
          setError(`Error loading countries, ${response.status}`);
        }
      } catch (error) {
        console.error(error);
        setError('Failed to load countries');
      }
    };

    if (!countriesFromStorage) {
      getCountries();
    } else {
      setCountries(JSON.parse(countriesFromStorage));
    }

    setLoading(false);
  }, []);

  return {
    countries,
    loading,
    error,
  };
}

If I change the toHaveBeenCalledTimes(1) to toHaveBeenCalledTimes(0), all of a sudden I get a Warning: An update to TestComponent inside a test was not wrapped in act(...). on

      29 |         if (response.status === 200) {
    > 30 |           setCountries(response.data);

And if I do any number higher than 1, it times out. Even if I extend the timeout time to 30 seconds, it just times out. What is happening here. I just don't understand. And all of that makes me wonder if it is even actually running the test correctly.


Solution

  • Alright, I think I figured it out. For some reason the wait for does not work in this situation. I am now doing it as follows and that works in all scenarios:

    describe('useCountries', () => {
      describe('when initialising without anything in the sessionStorage', () => {
        beforeEach(() => {
          axios.get.mockResolvedValue({ data: [['NL', 'Netherlands'], ['CU', 'Cuba']], status: 200 });
          global.sessionStorage = jest.fn();
          global.sessionStorage.getItem = jest.fn();
        });
    
        afterEach(() => {
          jest.clearAllMocks();
        });
    
        it('calls session storage set item once', async () => {
          const setItemMock = jest.fn();
          global.sessionStorage.setItem = setItemMock;
    
          const { waitForNextUpdate } = renderHook(() => useCountries());
          await waitForNextUpdate();
    
          expect(setItemMock).toHaveBeenCalledTimes(1);
        });
      });
    });
    

    So it seems that testing library wants you to just wait for the first update that happens and not until it stops doing things. As soon as it waits for the final results, other updates trigger and those are somehow messing up something internally.. I wish I could be more explicit about why, but as least waitForNextUpdate seems to have fixed my issue.