Search code examples
javascriptpromisejestjs

Jest fake timers with promises


I'm having a little trouble getting the Jest testing framework (version 23.2.0) to work nicely when using a combination of fake timers and promises. Where am I going wrong?

Let's say I have the following module:

// timing.js

export const timeout = ms =>
  new Promise(resolve => {
    setTimeout(resolve, ms)
  })

And my test file looks like:

// timing.test.js

import { timeout } from './timing'

describe('timeout()', () => {
  beforeEach(() => {
    jest.useFakeTimers()
  })

  it('resolves in a given amount of time', () => {
    const spy = jest.fn()

    timeout(100).then(spy)
    expect(spy).not.toHaveBeenCalled()

    jest.advanceTimersByTime(100)
    expect(spy).toHaveBeenCalled()
  })
})

This fails with the following output:

● timeout › resolves in a given amount of time

expect(jest.fn()).toHaveBeenCalled()

Expected mock function to have been called, but it was not called.

  15 |
  16 |     jest.advanceTimersByTime(100)
> 17 |     expect(spy).toHaveBeenCalled()
     |                 ^
  18 |   })
  19 | })
  20 |

  at Object.<anonymous> (src/timing.test.js:17:17)

However, if I remove the promise:

// timing.js
export const timeout = ms => ({
  then: resolve => {
    setTimeout(resolve, ms)
  }
})

... the test will pass

timeout
  ✓ resolves in a given amount of time (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.304s

UPDATE

Although it's not the most elegant solution, I'm currently using the below test instead. It works, but I'm still intrigued why the original one didn't

import { timeout } from './timing'

describe('timeout', () => {
  it('resolves in a given amount of time', done => {
    setTimeout(() => done(new Error('it didn\'t resolve or took longer than expected')), 10)
    return timeout(9).then(done)
  })
})

Solution

  • The current best alternative is to use the async versions of fake-timers. So you would do

    await clock.tickAsync(1000); // doesn't wait 1000ms but is async
    

    Instead of calling clock.tick. Please see the answer below for more details.

    At the moment, it's not supported

    You're not doing anything wrong - it doesn't work at the moment - sorry. The following things have to happen before this will work from our end:

    • Jest needs to merge the ongoing work to merge lolex as their fake timer implementation here https://github.com/facebook/jest/pull/5171
    • Lolex needs to support pumping through promises - we've discussed this with the V8 team in a recent Node.js collaborator summit. That would expose a hook we'll use to allow doing something like advanceTimeByTime(100) and have that work with promises.

    The problem in a gist is that the .then(spy) only gets called later.

    As we are volunteers - there is no concrete timeline for these things. I hope SimenB does the merge in the coming 2-3 months and I'll follow up with the hook with the V8 team next month.

    What you can do now

    You can always write an async test:

    // note this is an async function now
    it('resolves in a given amount of time', async () => {
      // this is in a promise.reoslve.then to not 'lock' on the await
      Promise.resolve().then(() => jest.advanceTimersByTime(100));
      await timeout(100);
    });
    

    You can add expectations after the timeout if there is anything else you want to wait for.