37

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)
  })
})
johngeorgewright
  • 1,005
  • 1
  • 9
  • 19

4 Answers4

37

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.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    Couldn't you do something like: const promise = createTimerPromise() jest.advanceTimersByTime(100) await promise – Marc Nov 13 '18 at 19:32
  • You could, it would be an extra line and I wanted to be able to await the function result directly. – Benjamin Gruenbaum Nov 14 '18 at 06:32
  • 1
    Does anyone knows if it's still true and Jest does not support it? – jesper Apr 04 '19 at 11:47
  • @jesper it's true and it still doesn't - you need to use the workaround. – Benjamin Gruenbaum Apr 04 '19 at 13:10
  • 1
    Yeah, that helped me a lot with testing a Promises. Thank you! – Vladyn Mar 07 '20 at 16:23
  • It really bothers me that this is still a problem in 2022. Posting here for anyone who stumbles upon this and goes "Oh, that's an old solution..".. Nope. Still relevant. – Brandon Faulkner Jan 10 '22 at 20:02
  • 1
    @BrandonFaulkner this is actually possible just not well documented sorry. All our timers (sinon, which are used in Jest) support async variants (so tick -> tickAsync) which works with promises. I should really update this answer sometime. – Benjamin Gruenbaum Jan 11 '22 at 09:40
  • Actually the answer below already mentions this so OP should just unaccept mine and accept @Aleksi 's answer isntead. – Benjamin Gruenbaum Jan 11 '22 at 09:41
  • Thanks for your response! I'm still pretty sure Jest doesn't support async variants (I guess they never exposed the sinon implementations). The below answer suggests to import "sinonjs/fake-timers" directly, which probably works, but doesn't change my disappointment that it can't be done simply with Jest :) – Brandon Faulkner Jan 11 '22 at 18:25
  • Make sure you opt into the new fake timer implementation which is the default in jest 27 :) – Benjamin Gruenbaum Jan 12 '22 at 08:56
  • Jest 29.5 (March 2023) now supports the async variants from fake-timers as well :) – oligofren May 10 '23 at 11:29
  • @oligofren feel free to edit my answer, btw we're adding native support to fake timers into Node core :) – Benjamin Gruenbaum May 11 '23 at 12:30
  • @BenjaminGruenbaum I know, I commented on the PR last week :p Will be interesting to see what this means long-term for Jest. – oligofren May 12 '23 at 05:23
  • @oligofren I'm also a sinon maintainer, so hopefully we'll make a version of fake-timers that uses that under the hood and jest will automagically get that :) – Benjamin Gruenbaum May 14 '23 at 16:05
  • 1
    @oligfren so am I :p should have stuck to the same nick everywhere, thought you recognized me: `fatso83` @github. – oligofren May 15 '23 at 08:32
  • Ah cool! Yeah sorry I didn’t recognize you – Benjamin Gruenbaum May 16 '23 at 14:09
15

Since jest@26.0.0 you can choose between two different fake timer implementations.

I found that jest.useFakeTimers('legacy') works with Promises using the flushPromises workaround, but it doesn't work with Date, whereas jest.useFakeTimers('modern') works with Date but not with Promises since await flushPromises() never resolves.

The best solution I found was to use @sinonjs/fake-timers instead, since that one works with both Promises and Date without any sort of workarounds or hacks:

import FakeTimers from "@sinonjs/fake-timers";

// Before tests:
const clock = FakeTimers.install();

// In tests:
await clock.tickAsync(100);

// After tests:
clock.uninstall();
Aleksi
  • 4,483
  • 33
  • 45
10

In my case the timer callback called other async functions so the other solution wasn't working for me. I ended up working out that by manually ensuring the promise queue was empty, all the async code will have finished running and I could get the tests to work:

function flushPromises() {
  // Wait for promises running in the non-async timer callback to complete.
  // From https://stackoverflow.com/a/58716087/308237
  return new Promise(resolve => setImmediate(resolve));
}

test('example', async () => {
  jest.useFakeTimers();

  example_function_to_set_a_timer();

  // Wait for one virtual second
  jest.advanceTimersByTime(1000);

  // Wait for any async functions to finish running
  await flushPromises();

  // Continue with tests as normal
  expect(...);
});
Malvineous
  • 25,144
  • 16
  • 116
  • 151
1

On March 6 2023 Jest added the async timers API in release 29.5!

Jest relies on the Sinon project's fake-timers library internally, so it has actually had access to the functionality for half a decade, but someone needed to step up and do the work to expose it on the Jest instance. That did not happen until now.

That means that Ben's answer (from 2018) mentioning using @sinonjs/fake-timers's tickAsync(ms) can now be used in Jest directly:

await jest.advanceTimersByTimeAsync(6000)
oligofren
  • 20,744
  • 16
  • 93
  • 180