Testing hooks which throw errors

5.5k views Asked by At

The [deprecated?] react-hooks-testing-library would return any errors thrown by the hook under test.

Probably my misunderstanding, but it looks like implementation now in the main @testing-library/react lost this feature?

Here's what I'm thinking:

import { safeRenderHook } from './safeRenderHook';

function useFail() {
  throw 'fail';
}

function useSucceed() {
  return 'success';
}

it('should fail', () => {
  const { result, error } = safeRenderHook(() => useFail());
  expect(error.current).toEqual('fail');
  expect(result.current).toBeUndefined();
});

it('should succeed', () => {
  const { result, error } = safeRenderHook(() => useSucceed());
  expect(result.current).toEqual('success');
  expect(error.current).toBeUndefined();
});

... and maybe an implementation like this?

import { render } from '@testing-library/react';
import React from 'react';

/**
 * A variant of `renderHook()` which returns `{ result, error }` with `error`
 * being set to any errors thrown by the hook. Otherwise, it behaves the same as
 * `renderHook()`.
 *
 * ```
 * const useFail = () => Promise.reject('fail!');
 *
 * it('should fail') {
 *  const { error } = safeRenderHook(() => useFail());
 *  expect(error).toEqual('fail!');
 * }
 * ```
 *
 * >Note: since this effectively swallows errors, you should be sure to
 * explicitly check the returned `error` value.
 */
export function safeRenderHook(renderCallback, options = {}) {
  const { initialProps = [], wrapper } = options;
  const result = React.createRef();
  const error = React.createRef();

  function TestComponent({ hookProps }) {
    let pendingError;
    let pendingResult;

    try {
      pendingResult = renderCallback(...hookProps);
    } catch (err) {
      pendingError = err;
    }

    React.useEffect(() => {
      result.current = pendingResult;
      error.current = pendingError;
    });

    return null;
  }

  const { rerender: baseRerender, unmount } = render(<TestComponent hookProps={initialProps} />, { wrapper });

  function rerender(rerenderCallbackProps) {
    return baseRerender(<TestComponent hookProps={rerenderCallbackProps} />);
  }

  return { result, error, rerender, unmount };
}

ps: I actually made a type-safe version of this if anyone's interested - but the type annotations make the example a bit harder to read on SO.

2

There are 2 answers

0
M15sy On

If you are using Jest you could do:

import { renderHook } from '@testing-library/react'

function useFail() {
  throw new Error('Oops')
}

it('should throw', () => {
  expect(() => {
    renderHook(() => useFail())
  }).toThrow(Error('Oops'))
})
0
poeticGeek On

Using expect.toThrow did not help me either. The hook is rendered multiple times (not sure why) and then exits without an error.

Following this comment, I was able to solve the issue with:

vitest-setup.js


const suppressConsoleError = () => {
    const errorSpy = vi.spyOn(console, "error")
        .mockImplementation(() => {
            //do nothing
        });

    return () => errorSpy.mockRestore();
};

global.renderHookWithError = (...args) => {
    let error;
    const restore = suppressConsoleError();

    try {
        renderHook(...args);
    } catch (ex) {
        error = ex;
    }

    restore();
    throw error;
};

myhook.js


export default useMyHook = (var) => {

  if (!var) {
     throw new Error("no var");
  }

  ...
};

myhook.test.js


   it("should throw", () => {
     expect(() => {
        renderHookWithError(useMyHook);
      }).toThrow("no var");

   });