4

I've got this custom hook:

import React from 'react';
import { useMessageError } from 'components/Message/UseMessage';

export interface Country {
  code: string;
  name: string;
}

export default function useCountry(): Array<Country> {
  const [countries, setCountries] = React.useState<Country[]>([]);
  const { showErrorMessage } = useMessageError();

  React.useEffect(() => {
    fetch('/api/countries', {
      method: 'GET',
    })
      .then(data => data.json())
      .then(function(data) {
        // ..
      })
      .catch(() => showErrorMessage());
  }, []);

  return countries;
}

I want to test catching an error if there will be invalid response. With that, error message should appear thanks to showErrorMessage(). And I've got this test:

const showErrorMessage = jest.fn();

jest.mock('components/Message/UseMessage', () => ({
  useMessageError: () => ({
    showErrorMessage: showErrorMessage,
  }),
}));

import useCountry from 'components/Country/useCountry';
import { renderHook } from '@testing-library/react-hooks';
import { enableFetchMocks } from 'jest-fetch-mock';
enableFetchMocks();

describe('The useCountry hook', () => {
  it('should show error message', async () => {
    jest.spyOn(global, 'fetch').mockImplementation(() =>
      Promise.resolve({
        json: () => Promise.reject(),
      } as Response),
    );

    const { result, waitForNextUpdate } = renderHook(() => useCountry());
    await waitForNextUpdate();

    expect(fetch).toHaveBeenCalled();
    expect(showErrorMessage).toHaveBeenCalled();
    expect(result.current).toEqual([]);
  });
});

But with that, I'm getting an error:

Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error

What I'm doing wrong in here? I assume it is somehow related with await waitForNextUpdate();, but I really don't know for sure and how to manage with it.

Jazi
  • 6,569
  • 13
  • 60
  • 92
  • it is for sure somehow related to process being stuck at `await waitForNextUpdate();` Have you tried to debug it? If you're on Webstorm, just right-click on test and use "debug" and move forward step by step. – Marek Urbanowicz Mar 29 '20 at 16:44

1 Answers1

2

waitForNextUpdate() waits for next update but your hook does not trigger it since it only calls showErrorMessage(). Take a look at this sandbox

As a straightforward solution something that triggers an update can be added:

  React.useEffect(() => {
    fetch('/api/countries', {
      method: 'GET',
    })
      .then(data => data.json())
      .then(function(data) {
        // ..
      })
      .catch(() => { 
        showErrorMessage();
        // trigger update in any suitable way, for example:
        setCountries([]); 
      });
  }, []);

But it may be better to refactor it in some way. For example, you could use a separate hook and state for errors:

export default function useCountry(): Array<Country> {
  const [countries, setCountries] = React.useState<Country[]>([]);
  const [error, setError] = React.useState(null);
  const { showErrorMessage } = useMessageError();

  React.useEffect(() => {
    fetch('/api/countries', {
      method: 'GET',
    })
      .then(data => data.json())
      .then(function(data) {
        // ..
      })
      .catch(() => setError(true));
  }, []);
  
  React.useEffect(() => {
    if (error) {
      showErrorMessage()
    }
  }, [error]);

  return countries;
}
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
Shlang
  • 2,495
  • 16
  • 24
  • It indeed works. Thanks! But I've got one question. Is it a good practice to add something updating component just only to make test work, like in this case? – Jazi Mar 30 '20 at 17:17
  • 1
    @KrzysztofTrzos no, it is not a good practice to do it only for testing purpose, but usually dealing with asynchronous operations you have to handle things like preventing multiple requests, race conditions and cancellation so you need to some additional state and updates anyway. – Shlang Mar 30 '20 at 18:37