question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

useFakeTimers breaks with native promise implementation

See original GitHub issue

🐛 Bug Report

Use the native Promise implementation breaks useFakeTimers

To Reproduce

Steps to reproduce the behavior:

jest.useFakeTimers();

test('timing', () => {
  Promise.resolve().then(() => console.log('promise'));
  setTimeout(() => console.log('timer'), 100);
  jest.runAllTimers();
  console.log('end');
});

Expected behavior

It should log:

  • promise
  • timer
  • end

This is because runAllTimers should trigger the async promise handler first, then the timeout delayed by 100ms, then return control.

Actual Behaviour

  • timer
  • end
  • promise

Link to repl or repo (highly encouraged)

https://repl.it/repls/PhysicalBriefCores

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:34
  • Comments:20 (6 by maintainers)

github_iconTop GitHub Comments

19reactions
KamalAmancommented, Feb 14, 2019

I would like to expand on this issue since it gets amplified by uses of setTimeouts within the async code:

jest.useFakeTimers();

test('timing', async () => {
  const shouldResolve = Promise.resolve()
    .then(() => console.log('before-promise'))
    .then(() => new Promise(r => setTimeout(r, 20)))
    .then(() => console.log('after-promise'));

  setTimeout(() => console.log('timer'), 100);
  jest.runAllTimers();
  await shouldResolve;
  console.log('end');
});

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

Expected: before-promise -> after-promise -> timer -> end Actual: timer -> before-promise -> Hangs

This issue here is there is nothing to continuously advance the timers once you’re within the promise world. shouldResolve will never resolve.

Switching to global.Promise = require('promise'); does seem like does the trick to resolve the issue for this particular use case. However in practice we have found the it does not work for all use-cases.

The best solution without replacing promises that i have come up for this is a utility function to continuouslyAdvanceTimers. However your results will still be out of order.

const _setTimeout = global.setTimeout;
function continuouslyAdvanceTimers() {
  let isCancelled = false;

  async function advance() {
    while (!isCancelled) {
      jest.runOnlyPendingTimers();
      await new Promise(r => _setTimeout(r, 1));
    }
  }

  advance();
  return () => {
    isCancelled = true;
  };
}

jest.useFakeTimers();

test('timing', async () => {
  const shouldResolve = Promise.resolve()
    .then(() => console.log('before-promise'))
    .then(() => new Promise(r => setTimeout(r, 20)))
    .then(() => console.log('after-promise'));

  setTimeout(() => console.log('timer'), 100);
  const cancelAdvance = continuouslyAdvanceTimers();
  await shouldResolve;
  cancelAdvance();
  console.log('end');
});

Expected: before-promise -> after-promise -> timer -> end Actual: timer -> before-promise -> after-promise -> end

8reactions
tatethurstoncommented, Apr 30, 2020

Posting this work around in case it helps someone else:

await Promise.resolve().then(() => jest.advanceTimersByTime(milliseconds));

More context here: https://stackoverflow.com/questions/51126786/jest-fake-timers-with-promises/51132058#51132058

Broader example:

  function sleep(ms: number): Promise<void> {
    return new Promise((resolve) => {
      setTimeout(resolve, ms);
    });
  }

  export async function foo(fn: () => T, waitMs: number): Promise<T> {
     await sleep(waitMs);
     return fn();
  }
  it('calls fn after x milliseconds', async () => {
    jest.useFakeTimers();

    const fn = jest.fn(() => 3);
    const retVal = foo(fn, 1000);

    expect(fn).not.toBeCalled();
    await Promise.resolve().then(() => jest.advanceTimersByTime(1000));
    expect(fn).toHaveBeenCalledTimes(1);
    await expect(retVal).resolves.toBe(3);
  });
Read more comments on GitHub >

github_iconTop Results From Across the Web

Jest fake timers with promises - Stack Overflow
I found that jest.useFakeTimers('legacy') works with Promises using the flushPromises workaround, but it doesn't work with Date , whereas jest.
Read more >
Jest 27: New Defaults for Jest, 2021 edition
We introduced a few more small breaking changes to help you avoid mistakes by disallowing some things that can easily happen unintentionally:.
Read more >
Testing Recipes - React
However, these testing strategies don't depend on implementation details, ... Use the asynchronous version of act to apply resolved promises await act(async ...
Read more >
Promise.race() - JavaScript - MDN Web Docs
The Promise.race() method takes an iterable of promises as input and ... how Promise.race() can be used to race several timers implemented ......
Read more >
Jest で Promise + setTimeout のテストがつまる件 - Qiita
Jest で Promise の返り値のテストを書いていたときに setTimeout が絡むと非同期の ... useFakeTimers breaks with native promise implementation
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found