Reputation: 434
I have a cutom hook that makes an API call on mount and handles state (isLoading, isError, data, refetch);
The hook is quite simple:
const useFetch = (endpoint, options) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [trigger, setTrigger] = useState(true);
const triggerSearch = () => {
setTrigger(!trigger);
};
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${process.env.API_URL}${endpoint}`
);
const json = await response.json();
setData(json);
setIsLoading(false);
} catch (error) {
setError(error);
setIsLoading(false);
}
};
fetchData();
}, [endpoint, trigger]);
return {
data,
isLoading,
error,
triggerSearch,
};
};
When trying to test the hook, I'm using jest and testing-library/react.
With react 18, the react-hooks from testing-library is no longer supported so I cannot use awaitForNextUpdate from renderHook as it doesn't return it.
Instead, we should use act and waitFor - which I have done and tests pass.
The problem is that I get the following error
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
test("should make an API call on mount", async () => {
const hook = renderHook(() => useFetch("/api"));
await act(async () => {
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
});
expect(fetch).toHaveBeenCalledTimes(1);
expect(getAccessTokenSilently).toHaveBeenCalledTimes(1);
expect(hook.result.current.data).toEqual({ message: "Test" });
expect(hook.result.current.isLoading).toEqual(false);
expect(hook.result.current.error).toEqual(null);
});
Could someone please point me in the right direction? I have tried removing all of the assertions and just calling renderHook, which also results in the same error.
Upvotes: 7
Views: 17167
Reputation: 1098
In case other people run into this and none of the above solutions worked, I had to wrap my expect
logic inside a synchronous waitFor
which was then wrapped inside an async act
. For some reason the other combinations did not work, especially in cases where I was waiting for my result
object from the hook to update its state (e.g. when result.current.someProperty changed during re-renders). This could point to an issue with the underlying hook itself, but everything was working great prior to updating React to version 18. This was for a migration from React 17 to React 18 using React Testing Library version 13.3.0
:
describe("useFetch", () => {
test("should make a call to the API and return the message", async () => {
const { result } = renderHook(() => useFetch("/api"));
await act(() => {
waitFor(() => {
expect(fetch).toHaveBeenCalledTimes(1);
expect(getAccessTokenSilently).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual({ message: "Test" });
expect(result.current.isLoading).toEqual(false);
expect(result.current.error).toEqual(null);
}
}
});
});
Upvotes: 0
Reputation: 321
Little bit late to the party but I've been struggling with the same issue for some time now and wanted to share my solution that doesn't involve calling the act method and also has typescripts type safety support.
import { renderHook, waitFor } from '@testing-library/react';
...
it('should set data correctly', async () => {
const { result } = renderHook<useFetchPayload<Movie>, useFetchProps>(useFetch<Movie>, {
initialProps: { url: 'success' },
});
await waitFor(() => {
expect(result.current.data).toBe(mockedMovieResponse);
expect(result.current.error).toBe(null);
expect(result.current.loading).toBe(false);
});
});
Notice that I use the renderHook and waitFor methods from @testing-library/react instead of the 'old way' using @testing-library/react-hooks as suggested in their own README.md.
Since v18 the renderHook method from react-hooks has been implemented by the react team. Using it with waitFor is the preferred way instead of using act (or the old waitForNextUpdate).
Upvotes: 1
Reputation: 735
Using act to call testing library methods is discouraged.
It's better to call the callback itself.
Upvotes: 2
Reputation: 434
So the way I resolved the issue was to include everything that calls setState function inside async act (including the initialization of renderhook, because it calls the api via useEffect):
describe("useFetch", () => {
test("should make a call to the API and return the message", async () => {
let hook;
await act(async () => {
hook = renderHook(() => useFetch("/api"));
});
const { result } = hook;
expect(fetch).toHaveBeenCalledTimes(1);
expect(getAccessTokenSilently).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual({ message: "Test" });
expect(result.current.isLoading).toEqual(false);
expect(result.current.error).toEqual(null);
});
});
Oh and make sure you are importing act from @testing-library/react
Upvotes: 13
Reputation: 1738
Something like this might work.
test("should make an API call on mount", async () => {
const hook = renderHook(() => useFetch("/api"));
// remove the act method
// you are not triggering an user action
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
expect(getAccessTokenSilently).toHaveBeenCalledTimes(1);
expect(hook.result.current.data).toEqual({ message: "Test" });
expect(hook.result.current.isLoading).toEqual(false);
expect(hook.result.current.error).toEqual(null);
});
Or you could try advancing timers:
beforeAll(() => {
// we're using fake timers because we don't want to
// wait a full second for this test to run.
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test("should make an API call on mount", () => {
const hook = renderHook(() => useFetch("/api"));
//advance timers by 1 second
jest.advanceTimersByTime(1000);
// remove the act method
// you are not triggering an user action
expect(fetch).toHaveBeenCalledTimes(1);
expect(getAccessTokenSilently).toHaveBeenCalledTimes(1);
expect(hook.result.current.data).toEqual({ message: "Test" });
expect(hook.result.current.isLoading).toEqual(false);
expect(hook.result.current.error).toEqual(null);
});
In my case I was using a click event to update the DOM and I had to use act
during the fireEvent
click event like so:
it('open toggler assessment modal when clicking card button', async () => {
renderTeacherAssessments();
const assessmentCard = screen.getAllByTestId('assessment-card');
const assessmentCardButton = assessmentCard[0].querySelector('button');
act(() => {
assessmentCardButton.click();
});
await waitFor(() => {
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument();
});
});
I've read in other posts regarding to the act
warning where most of them would be fixed by just using await waitFor...
wrapping the expect
method, but now for React18 and the latest RTL I'm not sure that still works.
Upvotes: 0