Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
  })
})
like image 587
johngeorgewright Avatar asked Jul 01 '18 20:07

johngeorgewright


People also ask

How do you use fake timer Jest?

Enable Fake Timers​useFakeTimers() . This is replacing the original implementation of setTimeout() and other timer functions. Timers can be restored to their normal behavior with jest. useRealTimers() .

How do you mock a promise in Jest?

In order to mock asynchronous code in Jest, more specifically Promises, you can use the mockResolvedValue function. This will mock the return value of the Promise to be 42. In order to test a Promise in Jest, you need to turn your it block into async in order to use the await keyword in front of an expect statement.

How do you test time in Jest?

jest-date-mock is a complete javascript module wrote by me, and it is used to test Date on jest. import { advanceBy, advanceTo } from 'jest-date-mock'; test('usage', () => { advanceTo(new Date(2018, 5, 27, 0, 0, 0)); // reset to date time. const now = Date.

How do I increase Jest timeout?

Use Jest. Settimeout(Newtimeout) To Increase The Timeout Value, If This Is A Long-Running Test.” will be demonstrated using examples from the programming language. jest. setTimeout(30000);


3 Answers

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.

like image 190
Benjamin Gruenbaum Avatar answered Sep 26 '22 06:09

Benjamin Gruenbaum


Since [email protected] 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(); 
like image 43
Aleksi Avatar answered Sep 26 '22 06:09

Aleksi


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(...);
});
like image 33
Malvineous Avatar answered Sep 26 '22 06:09

Malvineous