Hey there! I'm having an issue testing a custom hook that uses an async function in the useEffect hook. If I try to await a promise inside of the run function, my test times out if I use waitForNextUpdate. What am I doing wrong and how can I fix this behavior?
import { renderHook, act } from '@testing-library/react-hooks';
import { useState, useEffect, useContext } from 'react';
// using fake timers
jest.useFakeTimers();
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const run = async () => {
// await Promise.resolve(); // If I remove this line, test passes.
setTimeout(() => {
setCount(count => count + 1);
}, 5000);
};
run();
}, []);
return { count };
}
test('it should work', async () => {
const { result, waitForNextUpdate } = renderHook(() => Counter());
act(() => {
jest.runAllTimers();
});
// await waitForNextUpdate(); this line triggers the Jest 5000ms timeout error.
expect(result.current.count).toEqual(1);
});
I'm not very familiar with mocking timers myself, but I think if you have called jest.runAllTimers() then the update should have occurred and there is nothing to wait for.
waitForNextUpdate is used when you want to asynchronously wait for the timeout to actually trigger.
If expect(result.current.count).toEqual(1) is not passing by just running the timers, then I'll take a closer look. I'm wondering if the function hoisting that JavaScript does means using that using setTimeout in a function in the same file as running useFakeTimers won't pick up the mocked timers (because the function gets declared first and captures the original setTimout), but I'll admit I'm far from an expert on the finer details of JavaScript execution.
@mpeyper The test is not passing by just running the timers. Just to reiterate, the test fails if I try to await the promise in this function used in useEffect :
const run = async () => {
// waiting for the promise and having a setTimeout causes the test to to fail
await Promise.resolve();
setTimeout(() => {
setCount(count => count + 1);
}, 5000);
};
Hmm, ok. I'll take a look after the kids go to bed tonight.
Ok, so I know why it isn't working. It basically boils down to when waitForNextUpdate resolves vs. when you need to call jest.runAllTimers(). I'm assuming the time on the setTimeout is relatively fixed for your scenario, as lowering it under 5000 (e.g. 1000), removing the fake timers and just letting the waitForNextUpdate do it's thing allows the test to pass (albeit after a second of waiting), so I'll work on the understanding that using a mocked timer is important.
Your test follows the following sequence of events:
renderHook calledrenderHook exitsjest.runAllTimers() called (no-op - timer hasn't started)waitForNextUpdatesetTimeout calledThe deadlock occurs here because waitForNextUpdate does not resolve until the next render of the hook, and the set timeout wont fire until you call jest.runAllTimers(), which has already been and gone because the promise causes it to miss a beat.
My initial reaction, was oh, that's easy, I'll just wait first for the promise first, then run the timers, but unfortunately this also doesn't work because there is not setState or other render trigger between awaiting the promise and setting the timeout, so again, the test times out waiting.
My next thought was that I could use one of the other async utils, waitForValueToChange to periodically test for result.current.counterto change and throw a cheekyjest.runAllTimers()` in the callback to allow the timeout to fire in between checks, like so:
const { result, waitForValueToChange } = renderHook(() => Counter());
await waitForValueToChange(() => {
jest.runAllTimers();
return result.current.count;
});
expect(result.current.count).toEqual(1);
Unfortunately, it still times out. This time it's because I forgot that both wait and waitForValueToChange are built on top of waitForNextUpdate as their primitive utility so nothing is checked if the hook doesn't render.
Finally, I was able to get the test to pass by delaying when jest.runAllTimers() is called using setImmediate:
const { result, waitForNextUpdate, waitForValueToChange } = renderHook(() => Counter());
setImmediate(() => {
act(() => {
jest.runAllTimers();
});
});
await waitForNextUpdate();
expect(result.current.count).toEqual(1);
Now the test follows this sequence of events:
renderHook calledrenderHook exitswaitForNextUpdatesetTimeout calledjest.runAllTimers() calledsetState calledwaitForNextUpdate resolvesresult.current.counter === 1This works, but is very brittle for changes to the hook's flow and is definitely testing implementation details (which we should try to avoid).
I'm not 100% sure how to proceed on this one. The waitForValueToChange utility is designed to work on changes to the result.current values (technically you could wait for any value to change, but it's not a supported use case), and the wait utility is designed for a similar use case but when exceptions are involved, so I'm not sure if the semantics of when the checks run are actually wrong.
I'm actually struggling to think of any reason other than mixing promises and mocked timers that I would need to wait an arbitrary amount of time. Perhaps there is a missing concept in our API for handling this kind of thing? Perhaps some/all of the async utils should run checks on a timer instead of renders (or perhaps both)?
I'll think on this and I'm happy to take suggestions and feedback in this issue.
Thank you for @mpeyper !
I my case I used jest.useFakeTimers() instead of jest.runAllTimers() and it works perfectly.
I was having trouble as well, specifically with setInterval inside a useLayoutEffect.
Extra information
react-hooks-testing-library version: 3.2.1react-test-renderer version: 16.13.0react version: 16.13.0node version: 12.14.0yarn version: 1.22.0typescript version: 3.8.3In the git repo.
I ran a setInterval inside a useLayoutEffect (same problem with useEffect) hook and tried to advance it with jest.advanceTimersToNextTimer and jest's mock timers.

No codesandbox (jest.useFakeTimers is not implemented there) but I have a repo.
anyone knows how to properly test these kind of implementations?
fakeTimers() didn't work for me...
@giacomocerquone can you elaborate on what your hook/test look like?
For what it's worth, I've made a start on #393 so some of the issues will go away soon, but the chicken and egg problem of triggering an update while waiting for the change is unlikely to result in a a clean reading test. Open to idea on how you'd like to write your test, and see if we can make something work along those lines.
@mpeyper sorry but I'm too busy at work, if it's still needed I can recreate a repro
Yes please. Perhaps raise a new issue when you have time and I'll dig into the specifics of your situation there.
UseDelayEffect hook test. Hook is changing false on true with timeout
import { cleanup } from '@testing-library/react'
import { renderHook, act } from '@testing-library/react-hooks'
import useDelayEffect from './useDelayEffect'
jest.useFakeTimers()
// beforeAll(() => {})
// afterAll(() => {})
// beforeEach(() => {})
afterEach(() => {
cleanup()
jest.clearAllMocks()
})
describe('UseDelayEffect', () => {
it('useDelayEffect', async () => {
const { result, rerender } = renderHook(({ loading, delay }) => useDelayEffect(loading, delay), {
initialProps: { loading: true, delay: 1000 }
})
expect(result.current).toBeTruthy()
rerender({ loading: false, delay: 1000 })
setTimeout(() => {
expect(result.current).toBeFalsy()
}, 1000) // Pass
setTimeout(() => {
expect(result.current).toBeFalsy()
}, 900) // Fail
act(() => {
jest.runAllTimers()
})
rerender({ loading: true, delay: 5000 })
rerender({ loading: false, delay: 5000 })
setTimeout(() => {
expect(result.current).toBeFalsy()
}, 5000) // Pass
setTimeout(() => {
expect(result.current).toBeFalsy()
}, 4450) // Fail
act(() => {
jest.runAllTimers()
})
})
})
Hi @VladRose,
Can you share the useDelayEffect as well and perhaps a bit more explanation as to what your test is trying to achieve?
Most helpful comment
Ok, so I know why it isn't working. It basically boils down to when
waitForNextUpdateresolves vs. when you need to calljest.runAllTimers(). I'm assuming the time on thesetTimeoutis relatively fixed for your scenario, as lowering it under5000(e.g.1000), removing the fake timers and just letting thewaitForNextUpdatedo it's thing allows the test to pass (albeit after a second of waiting), so I'll work on the understanding that using a mocked timer is important.Your test follows the following sequence of events:
renderHookcalledrenderHookexitsjest.runAllTimers()called (no-op - timer hasn't started)waitForNextUpdatesetTimeoutcalledThe deadlock occurs here because
waitForNextUpdatedoes not resolve until the next render of the hook, and the set timeout wont fire until you calljest.runAllTimers(), which has already been and gone because the promise causes it to miss a beat.My initial reaction, was oh, that's easy, I'll just wait first for the promise first, then run the timers, but unfortunately this also doesn't work because there is not
setStateor other render trigger between awaiting the promise and setting the timeout, so again, the test times out waiting.My next thought was that I could use one of the other async utils,
waitForValueToChangeto periodically test for result.current.counterto change and throw a cheekyjest.runAllTimers()` in the callback to allow the timeout to fire in between checks, like so:Unfortunately, it still times out. This time it's because I forgot that both
waitandwaitForValueToChangeare built on top ofwaitForNextUpdateas their primitive utility so nothing is checked if the hook doesn't render.Finally, I was able to get the test to pass by delaying when
jest.runAllTimers()is called usingsetImmediate:Now the test follows this sequence of events:
renderHookcalledrenderHookexitswaitForNextUpdatesetTimeoutcalledjest.runAllTimers()calledsetStatecalledwaitForNextUpdateresolvesresult.current.counter === 1This works, but is very brittle for changes to the hook's flow and is definitely testing implementation details (which we should try to avoid).
I'm not 100% sure how to proceed on this one. The
waitForValueToChangeutility is designed to work on changes to theresult.currentvalues (technically you could wait for any value to change, but it's not a supported use case), and thewaitutility is designed for a similar use case but when exceptions are involved, so I'm not sure if the semantics of when the checks run are actually wrong.I'm actually struggling to think of any reason other than mixing promises and mocked timers that I would need to wait an arbitrary amount of time. Perhaps there is a missing concept in our API for handling this kind of thing? Perhaps some/all of the async utils should run checks on a timer instead of renders (or perhaps both)?
I'll think on this and I'm happy to take suggestions and feedback in this issue.