How to make jest fake timers work with asynchronous functions in jest/react?

1.5k views Asked by At
test('user is logged out when the server doesnt resond with a new access token', async () => {
    const spy = jest.spyOn(storageUtils, "getItemAndCheckExpiry")
    spy.mockImplementation(() => {return JSON.stringify({access: 'efrijreoireor', refresh: 'rufrijfreijriej'})})
    const history = createMemoryHistory()
    history.push('/auth')
    render(
        <Router history={history}>
        <AuthProvider >
            
            <App />
        
        </AuthProvider>
        </Router>
    )
    await waitFor(() => expect(history.location.pathname).toBe('/'))
    expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument()
    expect(spy).toHaveBeenCalledTimes(2)
    jest.useFakeTimers()
    jest.advanceTimersByTime(6000)
    expect(screen.queryByText(/Something went wrong/i)).not.toBeInTheDocument()
    
    // expect(timeOut).toHaveBeenCalledTimes(1)
    // expect(timeOut).toHaveBeenLastCalledWith(expect.any(Function), 4000);
    spy.mockRestore()

What this test should do is make sure that the message "something went wrong" disappears after 5 seconds. However, it doesn't work. I believe it is because I use fake timers inside an async function, but don't know how to solve this problem.

    }catch(err){
        logout(); 
        setUnexpectedLogoutError('Something went wrong and you were logged out.')
        setTimeout(() => {
            setUnexpectedLogoutError(null)
        }, 5000)
    }
}

That's how this functionality is implemented. Any help will be appreciated.

1

There are 1 answers

0
oligofren On

Jest uses the Sinon project's fake-timers library under the hood. This library has had the option of asynchronously ticking timers for years, but it was only on March 6 this year (2023) that Jest exposed the async API in release 29.5!

So if you wanted to do this in January 2022, you would either have had to use Sinon's fake timers API or another approach that would flush the pending tasks on the event loop (the async bits). Not that using @sinonjs/fake-timers is such a bad idea: I think that API is clearer to use and you get the latest features immediately, but I am hardly neutral, having maintained it for years, along with Simen Bekkhus of Jest fame and others :)

The 29.5 release added these methods:

advanceTimersToNextTimerAsync (nextAsync)
runAllTimersAsync (runAllAsync)
runOnlyPendingTimersAsync (runToLastAsync)
advanceTimersByTimeAsync(msToRun) (tickAsync(time))

You can probably solve your issue using the last one, just doing:

await jest.advanceTimersByTimeAsync(6000)

This just calls out to fake-timers' tickAsync under the hood

It's hard to be 100% certain, of course, without a fully reproducible snippet of code, though, but you can see me doing similar things in this test where I await the results of an async function that uses setTimeout internally.


Another approach that tries to flush promises manually can be seen in this answer). That will work as well, unless you have stubbed out setImmediate ...