react-hooks-testing-library icon indicating copy to clipboard operation
react-hooks-testing-library copied to clipboard

Cannot test hook raises an error

Open matiasgarcia opened this issue 1 year ago • 1 comments

Dependencies:

I have the following hook

function usePlan(): { loading: boolean, plan?: Plan, error?: Error } {
  const [loading, setLoading] = useState(true);
  const [plan, setPlan] = useState<Plan>();
  const [error, setError] = useState<Error>();

  useEffect(() => {
    const tryFetchSettlementFees = async () => {
      setLoading(true);

      try {
        const currentPlan = await client.getStorePlan();
        setPlan(currentPlan);
        setLoading(false);
        setError(undefined);
      } catch (error: any) {
        setError(error);
        setLoading(false);
        if(!axios.isAxiosError(error)) throw error;
      }
    };

    tryFetchSettlementFees();
  }, [setLoading, setPlan]);

  return {
    loading,
    error,
    plan,
  };
}

And I am trying to test the error is raised

	it.only('throws when effect fails due to something not request related', () => {
		const expectedError = new Error('expected error');
		client.getStorePlan = jest.fn().mockRejectedValueOnce(expectedError);

		let caughtError = null;
		class ErrorBoundary extends React.Component {
			constructor(props) {
				super(props)
				this.state = { hasError: false }
			}
	
			componentDidCatch(error) {
				this.setState({ hasError: true })
				caughtError = error
			}
	
			render() {
				return !this.state.hasError && this.props.children
			}
		}
	
		const wrapper = ({ children }) => <ErrorBoundary>{children}</ErrorBoundary>

		const vals = renderHook(() => usePlan(), { wrapper });

		expect(caughtError).toEqual(expectedError);
	});

Also tried

const expectedError = new Error('expected error');
client.getStorePlan = jest.fn().mockRejectedValueOnce(expectedError);
expect(() => renderHook(() => usePlan())).toThrow(expectedError);

Also tried

const expectedError = new Error('expected error');
client.getStorePlan = jest.fn().mockRejectedValueOnce(expectedError);
const { current } = renderHook(() => usePlan());
expect(current.error?.message).toEqual(...);

But in all of them I cannot seem to get the error I expect, which is the useEffect throwing the error. However, I get on jest an unhandledPromiseRejection so I might be doing something wrong:

node:internal/process/promises:265
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "Error: expected error".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

matiasgarcia avatar Apr 25 '23 15:04 matiasgarcia

So what is happening here is that your promise (tryFetchSettlementFees() will return a promise) is rejecting, but nothing is catching it so there is no way to test for it. Try changing the call in your effect to this to see what I mean:

tryFetchSettlementFees().catch((err) => console.error(err));

There isn't a way (that I know of) to capture the request promise into the effect itself without storing the result/error into state like you are doing, so you likely just want to move the throw outside of the effect, something like:

function usePlan(): { loading: boolean, plan?: Plan, error?: Error } {
  const [loading, setLoading] = useState(true);
  const [plan, setPlan] = useState<Plan>();
  const [error, setError] = useState<Error>();

  useEffect(() => {
    const tryFetchSettlementFees = async () => {
      setLoading(true);

      try {
        const currentPlan = await client.getStorePlan();
        setPlan(currentPlan);
        setLoading(false);
        setError(undefined);
      } catch (error: any) {
        setError(error);
        setLoading(false);
      }
    };

    tryFetchSettlementFees();
  }, [setLoading, setPlan]);

  if(error && !axios.isAxiosError(error)) throw error;

  return {
    loading,
    error,
    plan,
  };
}

(note this is entirely untested)

mpeyper avatar Apr 26 '23 06:04 mpeyper