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.

Proposed improvement for jest fake timers implementation

See original GitHub issue

🚀 Feature Proposal

Not sure if this should be Feature Proposal. Please let me know if I have used the wrong issue template. This is really an enhancement suggestion for the jest fake timers.

The enhancement suggestion is this: Make useFakeTimers and useRealTimers work as expected in every scenario.

Motivation

Presently the jest.useFakeTimers and jest.useRealTimers methods lead to surprising results.
This is due to how they are implemented. Here is my understanding of how they work:

  1. When the jest environment is created the methods that can be faked (setTimeout, etc) are captured by the jest-fake-timers package.
  2. When useFakeTimers is called the mockable methods (e.g. setTimeout) are replaced on the global object with a fake implementation
  3. When useRealTimers is called the mockable methods (e.g. setTimeout) are replaced with the captured real ones in #1

The problem is that when the useFakeTimers or useRealTimers is called modules imported by the test suite may have already captured the mockable methods and will hold on to those references so that calling useFakeTimers or useRealTimers puts the suite into a weird state where some of the loaded code is using real timers and some fake. This is never desirable.

I would like to propose that jest-fake-timers should instead always replace the mockable methods with a facade that will choose the correct implementation to defer to depending on the current desired state i.e. “real/fake”.

This would avoid the aforementioned problem as everything would hold the references to the facade and the switch will not change the global object references.

Example of real developer experience when using the existing api

This is a cut-down real-world experience that my team and I had. Hopefully it illustrates why this change is important:

We set timers property in jest.config set to ‘fake’ as the majority of our code base requires fake timers. However some tests need real timers. Here is how we attempted to solve this problem:

Attempt #1

import foo from './foo';
import waitForExpect from 'wait-for-expect';

test('foo does its thing', async () => {
  // Arrange
  jest.useRealTimers();

  // Act
  foo.doThing();

  // Assert
  await waitForExpect(() => {
    expect(foo.didItsThing()).toBeTrue();
  });
});

Result

Attempt failed because foo and waitForExpect already had fake setTimeout

Attempt #2

import foo from './foo';
import waitForExpect from 'wait-for-expect';

beforeEach(() => {
  jest.useRealTimers();
});

test('foo does its thing', async () => {
  // Arrange

  // Act
  foo.doThing();

  // Assert
  await waitForExpect(() => {
    expect(foo.didItsThing()).toBeTrue();
  });
});

Result

Attempt failed because foo and waitForExpect already had fake setTimeout

Attempt #3

jest.useRealTimers();
import foo from './foo';
import waitForExpect from 'wait-for-expect';

test('foo does its thing', async () => {
  // Arrange

  // Act
 foo.doThing();

  // Assert
  await waitForExpect(() => {
    expect(foo.didItsThing()).toBeTrue();
  });
});

Result

Attempt failed because foo and waitForExpect already had fake setTimeout Wait! What!?? Yeah this is because jest hoists imports ES module imports are hoisted so foo module is executed before the jest.useFakeTimers() call…

Attempt #4

foo.test.js:

import '/test-utilities/useRealTimers';
import foo from './foo';
import waitForExpect from 'wait-for-expect';

test('foo does its thing', async () => {
  // Arrange

  // Act
 foo.doThing();

  // Assert
  await waitForExpect(() => {
    expect(foo.didItsThing()).toBeTrue();
  });
});

/test-utilities/useRealTimers.js:

jest.useRealTimers();

Result

Success! However now we realize that we cannot mix the use of real timers and fake timers in a single test-suite…

Sample facade implemention

Here is some pseudo code to illustrate how the facades could work:

let useFakeTimers = false;

jest.useFakeTimers = () => (useFakeTimers = true); jest.useRealTimers = () => (useFakeTimers = false);

const realSetTimeout = global.setTimeout; global.setTimeout = (…args) => (useFakeTimers ? fakeSetTimeout : realSetTimeout)(…args);

Example

Exactly as before except now you can safely use jest.useFakeTimers and jest.useRealTimers anywhere and the system will behave as expected.

Pitch

Why does this feature belong in the Jest core platform? It is already in core.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:8

github_iconTop GitHub Comments

2reactions
jackgeekcommented, Jan 28, 2021

Hi @markdascher,

With my proposal if your utility was imported in jest-before-env-setup.js it would capture the real setTimeout. It could then be used in any test-suite and work as expected because the setTimeout captured in its first import (in the jest-before-env-setup.js) would be the real setTimeout.

This feature request is quite old now and I have noticed that jest seems to behave slightly differently now: Regardless of whether you are have called useFakeTimers or useRealTimers or configured real or fake timers as defaults, if your test is async then real timers are always used.

This is unfortunate as we still live in an environment where it is not possible to fake native promises. I have encountered situations, recently using https://mswjs.io/, where I want to use fake timers and have an async test. This is because msw is working in a separate thread and waiting on a promise is unavoidable. This forces all tests using msw to be async and now means they all run with real timers. 🤔

0reactions
github-actions[bot]commented, May 3, 2022

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Jest 28: Shedding weight and improving compatibility
Jest 26 introduced the concept of "modern" fake timers, which uses @sinonjs/fake-timers under the hood, and Jest 27 made it the default.
Read more >
Switch to modern fake timers implementation - GitLab.org
Modern fake timers offer many improvements over legacy implementation such as: custom faking of timers functions, i.e. we can tell Jest which ...
Read more >
Jest fake timers with promises - Stack Overflow
Jest needs to merge the ongoing work to merge lolex as their fake timer implementation here https://github.com/facebook/jest/pull/5171 ...
Read more >
Fix the "not wrapped in act(...)" warning with Jest fake timers
There aren't many places you need to manually use act if you're using React Testing Library's async utilities, but if you're using fake...
Read more >
Jest 27 Has New Default. Jest is a JavaScript testing ... - Sonika
Jest is a JavaScript testing framework maintained by Facebook. Default test environment from “jsdom” to “node”. Modern fake timers implementation will now ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

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