testing-library / react-hooks-testing-library

🐏 Simple and complete React hooks testing utilities that encourage good testing practices.
https://react-hooks-testing-library.com
MIT License
5.25k stars 230 forks source link

Cannot test hook raises an error #960

Open matiasgarcia opened 1 year ago

matiasgarcia commented 1 year ago

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'
}
mpeyper commented 1 year ago

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)