I was having this problem and came across this thread. I'm unit testing a hook but the principles should be the same if your async useEffect code is in a component. Because I'm testing a hook, I'm calling renderHook
from react hooks testing library. If you're testing a regular component, you'd call render
from react-dom
, as per the docs.
The problem
Say you have a react hook or component that does some async work on mount and you want to test it. It might look a bit like this:
const useMyExampleHook = id => {
const [someState, setSomeState] = useState({});
useEffect(() => {
const asyncOperation = async () => {
const result = await axios({
url: `https://myapi.com/${id}`,
method: "GET"
});
setSomeState(() => result.data);
}
asyncOperation();
}, [id])
return { someState }
}
Until now, I've been unit testing these hooks like this:
it("should call an api", async () => {
const data = {wibble: "wobble"};
axios.mockImplementationOnce(() => Promise.resolve({ data}));
const { result } = renderHook(() => useMyExampleHook());
await new Promise(setImmediate);
expect(result.current.someState).toMatchObject(data);
});
and using await new Promise(setImmediate);
to "flush" the promises. This works OK for simple tests like my one above but seems to cause some sort of race condition in the test renderer when we start doing multiple updates to the hook/component in one test.
The answer
The answer is to use act()
properly. The docs say
When writing [unit tests]... react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions.
So our simple test code actually wants to look like this:
it("should call an api on render and store the result", async () => {
const data = { wibble: "wobble" };
axios.mockImplementationOnce(() => Promise.resolve({ data }));
let renderResult = {};
await act(async () => {
renderResult = renderHook(() => useMyExampleHook());
})
expect(renderResult.result.current.someState).toMatchObject(data);
});
The crucial difference is that async act around the initial render of the hook. That makes sure that the useEffect hook has done its business before we start trying to inspect the state. If we need to update the hook, that action gets wrapped in its own act block too.
A more complex test case might look like this:
it('should do a new call when id changes', async () => {
const data1 = { wibble: "wobble" };
const data2 = { wibble: "warble" };
axios.mockImplementationOnce(() => Promise.resolve({ data: data1 }))
.mockImplementationOnce(() => Promise.resolve({ data: data2 }));
let renderResult = {};
await act(async () => {
renderResult = renderHook((id) => useMyExampleHook(id), {
initialProps: { id: "id1" }
});
})
expect(renderResult.result.current.someState).toMatchObject(data1);
await act(async () => {
renderResult.rerender({ id: "id2" })
})
expect(renderResult.result.current.someState).toMatchObject(data2);
})
react-testing-library
, therefore not having a relation with Enzyme. Seems like I can't do anything right now, thanks for the clarification. – Ralliact(() => { wrapper = mount(<App />); })
. React docs say it does some magic under the hood reactjs.org/blog/2019/02/06/react-v16.8.0.html – Humidistat