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:
- When the jest environment is created the methods that can be faked (setTimeout, etc) are captured by the
jest-fake-timers
package. - When
useFakeTimers
is called the mockable methods (e.g. setTimeout) are replaced on the global object with a fake implementation - 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:
- Created 4 years ago
- Comments:8
Top GitHub Comments
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. 🤔
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.