jest: "Modern" fake-timer implementation doesn't work with PromiseJS.

Awesome work on https://github.com/facebook/jest/pull/7776, thanks for that!!

šŸ› Bug Report

Iā€™m using Jest 26.1.0.

Testing async code with Promises, as described in this Jest doc, doesnā€™t seem to work with jest.useFakeTimers('modern'); (from https://github.com/facebook/jest/pull/7776; documented here) if youā€™re using the ā€˜promiseā€™ library from NPM.

For a while (starting in https://github.com/facebook/react-native/commit/3ff39870ce776c653823e1733363be0401896294), React Native has used that library as part of their Jest setup (which you get if you use preset: 'react-native'), so I expect this will affect most people using React Native (this is how I encountered it).

Otherwise, Iā€™m not sure how often people use that ā€˜promiseā€™ library, but itā€™s an official solution given in a Jest troubleshooting doc, and pops up as a workaround for some issues in this repo (e.g. here).

This is arguably a regression (or would be when ā€˜modernā€™ becomes the default), as this behavior isnā€™t observed with jest.useFakeTimers('legacy'); or jest.useRealTimers().

To Reproduce

E.g.,

global.Promise = require('promise');

jest.useFakeTimers('modern');

test('1 equals 1', async () => {
  await Promise.resolve();
  expect(1).toEqual(1);
});

Expected behavior

I expect the test to pass. Instead, five seconds go by and I get this failure:

  āœ• 1 equals 1 (5006 ms)

  ā— 1 equals 1

    : Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

      3 | jest.useFakeTimers('modern');
      4 | 
    > 5 | test('1 equals 1', async () => {
        | ^
      6 |   await Promise.resolve();
      7 |   expect(1).toEqual(1);
      8 | });

      at new Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22)
      at Object.<anonymous> (fetchData.test.js:5:1)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        5.762 s, estimated 6 s
Ran all test suites.
error Command failed with exit code 1.

Link to repl or repo (highly encouraged)

In the repro linked below, Iā€™ve closely imitated all the tests from the sections in the ā€œTesting Asynchronous Codeā€ doc that use Promises, to suggest that the failure happens in usual, well-documented ways of testing (provided youā€™re using PromiseJS).

In its current state, you should observe these timeout errors on tests that use Promises (just run yarn and yarn test).

Try uncommenting global.Promise = require('promise'); at the top of fetchData.test.js; the tests should all pass.

Try doing something other than jest.useFakeTimers('modern');; the tests should all pass.

(Incidentally, note that accidentally calling jest.useFakeTimers multiple times with different arguments in the same file seems disruptive and might hamper your investigation. Also, be sure to comment out jest.runAllTimers() if youā€™re using real timers.)

https://github.com/chrisbobbe/jest-async-modern-timers-repro

envinfo

  System:
    OS: macOS 10.15.5
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Binaries:
    Node: 10.20.1 - ~/.nvm/versions/node/v10.20.1/bin/node
    Yarn: 1.22.4 - /usr/local/bin/yarn
    npm: 6.14.4 - ~/.nvm/versions/node/v10.20.1/bin/npm
  npmPackages:
    jest: ^26.1.0 => 26.1.0 

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 31
  • Comments: 32 (3 by maintainers)

Commits related to this issue

Most upvoted comments

I have the same error with nock 12.0.3 and jest 26.1.0

The test case

test('nock', async () => {
    jest.useFakeTimers('modern');
    nock('http://modern').get("/").reply(200);
    await fetch(`http://modern`, { method: "GET" });
});

Failing :

 : Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:
  > 18 |         test('nock', async () => {
    |         ^
    19 |             jest.useFakeTimers('modern');
    20 |             nock('http://modern').get("/").reply(200);
    21 |             await fetch(`http://modern`, { method: "GET" });

Itā€™s working when I replace

  • ā€˜modernā€™ by ā€˜legacyā€™ or
  • fetch(ā€¦) by Promise.resolve() (nock is not actually used)

Is this moving along? As issue still persists.

I created a pull request to add the options object with an ignore property (notToFake) 100% inspired by the comment above. Any feedback is welcome and greatly appreciated.

The added code is kept deliberately simple and pragmatic, it can still be changed easily to reflect any input. https://github.com/facebook/jest/pull/11661

@tomdaniel0x01 thanks, I think I will use fake timers from sinon for now, as they seemed to work. But Iā€™d would rather get rid of another dependency and use it in jest. Its the only I am really missing in jest.

I have to add to this issue for clarity: It does not only affect non-native global.promise replacements, it also makes native promises in Node.JS with TypeScript stop working.

@SimenB any progress on this? It seems like the problem and solution are both known. Is there any reason a change should not be made that would stop mocking nextTick? Is this just waiting on someone to make the change?

As a test, can you modify your node_modules/@jest/fake-timers/build/modernFakeTimers.js (or wherever itā€™s located) from the line const toFake = Object.keys(this._fakeTimers.timers); to const toFake = Object.keys(this._fakeTimers.timers).filter(name => name !== 'nextTick');.

Yepā€”when I do this, it works! Looks like https://github.com/sinonjs/fake-timers/pull/323 is related.

As an aside, one should never replace global.Promise - that suggestion is outdated. E.g. when using async-await you will always get the native Promise regardless of the value of global.Promise.

Thanks for the heads-up! Yeah, a priori, Iā€™m not really interested in replacing global.Promise. But Iā€™m using React Native, with preset: 'react-native', so itā€™s being done whether I like it or not (RN has been doing this since https://github.com/facebook/react-native/commit/3ff39870ce776c653823e1733363be0401896294). I suppose I could make a PR to react-native and fork it in the meantime, if it came to that.

Hmm, I wonder if this is because we fake process.nextTick as well. As a test, can you modify your node_modules/@jest/fake-timers/build/modernFakeTimers.js (or wherever itā€™s located) from the line const toFake = Object.keys(this._fakeTimers.timers); to const toFake = Object.keys(this._fakeTimers.timers).filter(name => name !== 'nextTick');. That should exclude it. This is the default behavior of @sinonjs/fake-timers, which Iā€™ve changed in Jestā€™s implementation.


As an aside, one should never replace global.Promise - that suggestion is outdated. E.g. when using async-await you will always get the native Promise regardless of the value of global.Promise.

facing same issue. Have we fixed this issue or itā€™s still in progress?

Or the other way : keep everything mocked by default, and a way to exclude some methods ?

it('does not mock nextTick', () => {
  jest.useFakeTimers('modern')
  // `nextTick` is mocked by default
})

it('mocks nextTick', () => {
  jest.useFakeTimers('modern', { toNotFake: ['nextTick'] })
  // nextTick is not mocked when requested
})

Possible workaround for the time being is using https://github.com/sinonjs/fake-timers, as it was mention before. But hereā€™s the example for those who stumbles across this thread:

import FakeTimers, { InstalledClock } from "@sinonjs/fake-timers";

const now = new Date("2021-11-25 11:00:00");

let clock: InstalledClock;
beforeAll(() => {
  clock = FakeTimers.install({
    now: now,
    shouldAdvanceTime: true,
    toFake: ["Date"],
  });
});

afterAll(() => {
  clock.uninstall();
});

Whatā€™s the status? Will it be fixed?

@SimenB

If we stop mocking nextTick by default we need to allow people to mock it if they want (right now we mock everything, which I think is ideal, but it sometimes breaks other modules).

So it would be acceptable to stop mocking nextTick by default, as long as there is a way to make sure it gets mocked? Would an API similar to sinonā€™s for choosing what to mock work here?

The userā€™s code would then look something like this:

it('does not mock nextTick', () => {
  jest.useFakeTimers('modern')
  // `nextTick` is not mocked by default
})

it('mocks nextTick', () => {
  jest.useFakeTimers('modern', { toFake: ['nextTick'] })
  // nextTick is mocked when requested
})

For most cases here I assume a strategically placed jest.runAllTicks() would solve the issue, but itā€™s non-obvious

Is this just waiting on someone to make the change?

This, essentially šŸ™‚ However, the change to make is not obvious. If we stop mocking nextTick by default we need to allow people to mock it if they want (right now we mock everything, which I think is ideal, but it sometimes breaks other modules).