Last active
August 20, 2024 23:56
-
-
Save apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4 to your computer and use it in GitHub Desktop.
Async testing with jest fake timers and promises
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PLEASE CHECK THIS REPO WITH THE EXAMPLES THAT YOU CAN RUN: | |
https://github.com/apieceofbart/async-testing-with-jest-fake-timers-and-promises | |
// Let's say you have a function that does some async operation inside setTimeout (think of polling for data) | |
function runInterval(callback, interval = 1000) { | |
setInterval(async () => { | |
const results = await Promise.resolve(42) // this might fetch some data from server | |
callback(results) | |
}, interval) | |
} | |
// Goal: We want to test that function - make sure our callback was called | |
// The easiest way would be to pause inside test for as long as we neeed: | |
const pause = ms => new Promise(res => setTimeout(res, ms)) | |
it('should call callback', async () => { | |
const mockCallback = jest.fn() | |
runInterval(mockCallback) | |
await pause(1000) | |
expect(mockCallback).toHaveBeenCalledTimes(1) | |
}) | |
// This works but it sucks we have to wait 1 sec for this test to pass | |
// We can use jest fake timers to speed up the timeout | |
it('should call callback', () => { // no longer async | |
jest.useFakeTimers() | |
const mockCallback = jest.fn() | |
runInterval(mockCallback) | |
jest.advanceTimersByTime(1000) | |
expect(mockCallback).toHaveBeenCalledTimes(1) | |
}) | |
// This won't work - jest fake timers do not work well with promises. | |
// If our runInterval function didn't have a promise inside that would be fine: | |
function runInterval(callback, interval = 1000) { | |
setInterval(() => { | |
callback() | |
}, interval) | |
} | |
it('should call callback', () => { | |
jest.useFakeTimers() | |
const mockCallback = jest.fn() | |
runInterval(mockCallback) | |
jest.advanceTimersByTime(1000) | |
expect(mockCallback).toHaveBeenCalledTimes(1) // works! | |
}) | |
// What we need to do is to have some way to resolve the pending promises. One way to do it is to use process.nextTick: | |
const flushPromises = () => new Promise(res => process.nextTick(res)) | |
// IF YOU'RE USING NEW JEST (>27) WITH MODERN TIMERS YOU HAVE TO USE A SLIGHTLY DIFFERENT VERSION | |
// const flushPromises = () => new Promise(jest.requireActual("timers").setImmediate) | |
it('should call callback', async () => { | |
jest.useFakeTimers() | |
const mockCallback = jest.fn() | |
runInterval(mockCallback) | |
jest.advanceTimersByTime(1000) | |
await flushPromises() | |
expect(mockCallback).toHaveBeenCalledTimes(1) | |
}) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm using Jest v29 and
useFakeTimers
now allows us to specify what not to fake, e.g.jest.useFakeTimers({ doNotFake: ['nextTick'] })
.I'm testing a function that batches an array of network requests (
fetchChatSessionToken()
) into groups of 5 and performs each batch inside aPromise.all()
. It then usessetTimeout
for a 1 second delay to address rate limiting, before calling the next batch recursively. So in the below test case of 12 requests, there are 3 batches split up by two timeouts.I found that I had to call both
await new Promise(process.nextTick)
andjest.advanceTimersByTime(1000)
each time I wanted to advance to the next batch, so twice for a test with 3 expected batches. Also note that the test returns anexpect(promise).resolves...
, which may affect the number ofnextTick
events that you need.