I'm trying to understand Jest's asynchronous testing.
My module has a function which accepts a boolean and returns a Promise of a value. The executer function calls setTimeout
, and in the timed out callback the promise resolves or rejects depending on the boolean initially supplied. The code looks like this:
const withPromises = (passes) => new Promise((resolve, reject) => {
const act = () => {
console.log(`in the timout callback, passed ${passes}`)
if(passes) resolve('something')
else reject(new Error('nothing'))
}
console.log('in the promise definition')
setTimeout(act, 50)
})
export default { withPromises }
I'd like to test this using Jest. I guess that I need to use the mock timers Jest provides, so my test script looks a bit like this:
import { withPromises } from './request_something'
jest.useFakeTimers()
describe('using a promise and mock timers', () => {
afterAll(() => {
jest.runAllTimers()
})
test('gets a value, if conditions favor', () => {
expect.assertions(1)
return withPromises(true)
.then(resolved => {
expect(resolved).toBe('something')
})
})
})
I get the following error/failed test, whether or not I call jest.runAllTimers()
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Can you explain where I'm going wrong and what I might do to get a passing test that the promise resolves as expected?
The native timer functions (i.e., setTimeout() , setInterval() , clearTimeout() , clearInterval() ) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time.
You can also use the . resolves matcher in your expect statement, and Jest will wait for that promise to resolve. If the promise is rejected, the test will automatically fail. return expect(fetchData()).
To make a promise from setTimeout with JavaScript, we can use the Promise constructor. const later = (delay) => { return new Promise((resolve) => { setTimeout(resolve, delay); }); };
Promise. resolve(1) is a static function that returns an immediately resolved promise. setTimeout(callback, 0) executes the callback with a delay of 0 milliseconds.
The call to jest.useFakeTimers()
mocks every timer function with one that you must control. Instead of the timer running automatically, you would advance it manually. The jest.runTimersToTime(msToRun)
function would advance it by msToRun
milliseconds. It's very common that you want to fast-forward until every timer has elapsed and it would be cumbersome to calculate the time it takes for all the timers to finish, so Jest provides jest.runAllTimers()
, which pretends that enough time has passed.
The problem in your test is that you never call jest.runAllTimers()
in the test, but you call it in the afterAll
hook, which is called after the tests have finished. During your test the timer remains at zero so your callback is never actually called and Jest aborts it after a predefined interval (default: 5s) to prevent being stuck with a potentially endless test. Only after the test has timed out, you call jest.runAllTimers()
, at which point it doesn't do anything, since all tests have already finished.
What you need to do is launch the promise and then advance the timer.
describe('using a promise and mock timers', () => {
test('gets a value, if conditions favor', () => {
expect.assertions(1)
// Keep a reference to the pending promise.
const pendingPromise = withPromises(true)
.then(resolved => {
expect(resolved).toBe('something')
})
// Activate the timer (pretend the specified time has elapsed).
jest.runAllTimers()
// Return the promise, so Jest waits for its completion and fails the
// test when the promise is rejected.
return pendingPromise
})
})
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With