renderHook fails for suspense-based hooks using jest.useFakeTimers
marcospassos opened this issue · 1 comments
Problem description:
We have a cache-related test that requires mocking the setTimeout to assert the cache has been cleaned after a given time interval. However, the renderHook seems to do not work when using fake times.
react-hooks-testing-libraryversion: 5.1.1reactversion: 16.0.0react-domversion (if applicable): 16.0.0
What happened:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
Reproduction:
jest.useFakeTimers();
const cache: {promise?: Promise<any>, value?: any} = {};
const useExample = () => {
if (cache.value !== undefined) {
return cache.value;
}
if (!cache.promise) {
cache.promise = new Promise(resolve => setTimeout(() => resolve('foo'), 10))
.then(result => {
cache.value = result;
});
}
throw cache.promise;
};
it('should not fail', async () => {
const {result, waitForNextUpdate} = renderHook(() => useExample());
jest.runAllTimers();
await waitForNextUpdate();
expect(result.current).toBe('foo');
});Hi @marcospassos,
Disclaimer: I am not an expert in any of this so this is my understanding of what is going on but there may be some inaccuracies of mistakes in my mental model of it all.
For starters, this isn't an issue with @testing-library/react-hooks but rather trying to mix promises and fake timers with jest. Running jest.runAllTimers() does trigger the timeout to fire, however, the then (and other microtasks) of the promise does not run until the test awaits as it is always asynchronous.
You can work around this by:
- awaiting something else that will resolve to give your promise a chance to flush its microtasks
- using our
rerenderfunctionality to force the new value to be returned- I'm not actually sure why this is required as the promise resolving should be enough to trigger the render, but I suspect there is something deep in the guts of the react renderer (
react-dombased on the provided info) that needs the promise to be flushed.
- I'm not actually sure why this is required as the promise resolving should be enough to trigger the render, but I suspect there is something deep in the guts of the react renderer (
The simplest way I could find to do this was:
it("should not fail", async () => {
const { result, rerender } = renderHook(() => useExample());
jest.runAllTimers();
await Promise.resolve();
rerender()
expect(result.current).toBe("foo");
});Secondly, the async utils are used when the test needs to wait for async behaviour in a hook. By faking the timers, the updates are no longer asynchronous (excluding the above described weirdness of mixing timeouts and promises). An alternative approach would be to not use fake timers and allow an async util to actually wait for it to happen:
// do not call jest.useFakeTimers();
it("should not fail", async () => {
const { result, waitForNextUpdate } = renderHook(() => useExample());
await waitForNextUpdate()
expect(result.current).toBe("foo");
});This results in a slightly slower test, however, I feel that not faking timers allows the test to be more robust to changes in the implementation on the test.
I hope this helps.