Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fake-timers)!: allow jest.useFakeTimers() and projectConfig.fakeTimers to take an options bag #12572

Merged
merged 73 commits into from Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
6b663c8
feat!: allow useFakeTimers to take an options bag
mrazauskas Mar 12, 2022
344fda7
name `timersConfig` consistently
mrazauskas Mar 12, 2022
c42e93d
tweak defaults
mrazauskas Mar 12, 2022
d828b55
tweak error message
mrazauskas Mar 12, 2022
b6d30db
docs
mrazauskas Mar 13, 2022
3e487fc
add note on "strategy"
mrazauskas Mar 13, 2022
fa2c75f
add example lines
mrazauskas Mar 13, 2022
f480a6e
prettier
mrazauskas Mar 13, 2022
44fffd8
rename `timerLimit`
mrazauskas Mar 13, 2022
39b27f9
rename `fakeTimersConfig` and `FakeTimersConfig`
mrazauskas Mar 13, 2022
e4b0dc0
rethinking API
mrazauskas Mar 13, 2022
4ccc5bb
implement `enableGlobally` and `legacyFakeTimers`
mrazauskas Mar 14, 2022
5ccdff4
group e2e tests
mrazauskas Mar 14, 2022
e17d545
more docs
mrazauskas Mar 14, 2022
a671c6a
more cli docs
mrazauskas Mar 14, 2022
bb38197
clean up e2e tests
mrazauskas Mar 15, 2022
e3ec932
rename `fakeTimers`
mrazauskas Mar 15, 2022
1e33ec0
fix resetAllMocks and resetMocks e2e tests
mrazauskas Mar 15, 2022
8df6ae5
legacyFakeTimers test
mrazauskas Mar 15, 2022
78225a5
add timerLimit test
mrazauskas Mar 15, 2022
c61f4da
fix timerLimit
mrazauskas Mar 15, 2022
f9c511f
Merge branch 'main'
mrazauskas Mar 15, 2022
c96a4c9
add deprecation warning
mrazauskas Mar 15, 2022
3726701
implement advanceTimers option
mrazauskas Mar 15, 2022
84a2a0c
allow clearing not faked timers
mrazauskas Mar 15, 2022
5fef5ce
fix type tests
mrazauskas Mar 15, 2022
aa8d7a9
implement doNotFake option
mrazauskas Mar 16, 2022
f4758c5
fix messages
mrazauskas Mar 16, 2022
a1bc0f6
clean up CLI docs
mrazauskas Mar 16, 2022
4171178
even more docs
mrazauskas Mar 16, 2022
a3c7e4c
better errors
mrazauskas Mar 16, 2022
cd01710
add unit test
mrazauskas Mar 17, 2022
2b8ed49
changelog entry
mrazauskas Mar 17, 2022
0eabf14
better changelog entry
mrazauskas Mar 17, 2022
ebed0c1
remove performance from unit test
mrazauskas Mar 17, 2022
8b5d6b0
fix unit test
mrazauskas Mar 17, 2022
ef1434a
better Date.now() (?)
mrazauskas Mar 17, 2022
5ad74f4
expect.any(Number)
mrazauskas Mar 17, 2022
4ec6cce
mock Date.now()
mrazauskas Mar 17, 2022
7fb3de3
clean up
mrazauskas Mar 19, 2022
947ad20
fix config examples
mrazauskas Mar 19, 2022
268febd
clean up
mrazauskas Mar 19, 2022
5ba69dd
Update docs/JestObjectAPI.md
mrazauskas Mar 21, 2022
2b0a6fe
one more backticks
mrazauskas Mar 21, 2022
6c7e24c
clean up
mrazauskas Mar 22, 2022
a2dfb22
bump @types/sinonjs__fake-timers
mrazauskas Mar 23, 2022
1995027
rebase
mrazauskas Mar 24, 2022
01fabf4
docs
mrazauskas Mar 24, 2022
9e3d7bf
tweak types
mrazauskas Mar 24, 2022
73e092b
prettier
mrazauskas Mar 24, 2022
23196dd
fix docs
mrazauskas Mar 24, 2022
4354da8
now must be a number
mrazauskas Mar 24, 2022
8cc5b8c
true conditions first
mrazauskas Mar 24, 2022
c78df2e
more type tests
mrazauskas Mar 24, 2022
2168c49
more docs
mrazauskas Mar 25, 2022
d183309
refactor toSinonFakeTimersConfig
mrazauskas Mar 26, 2022
ca52b59
fix lint errors
mrazauskas Mar 26, 2022
fb61d57
tweak types
mrazauskas Mar 27, 2022
7113374
Merge branch 'main' into useFakeTimers-bag-of-options
mrazauskas Mar 28, 2022
aec5053
better mock
mrazauskas Mar 28, 2022
5c1daec
rework test
mrazauskas Mar 28, 2022
4fb53fd
fix bug
mrazauskas Apr 2, 2022
459b551
tweak docs
mrazauskas Apr 2, 2022
1009ff1
remove CLI option
mrazauskas Apr 3, 2022
72a6565
improve description
mrazauskas Apr 3, 2022
8ff79fa
snap
mrazauskas Apr 3, 2022
6d1c0d1
default configuration
mrazauskas Apr 4, 2022
cb6bede
fix resetMocks test
mrazauskas Apr 4, 2022
1b29f9e
fix resetMocks test again
mrazauskas Apr 4, 2022
20f1d34
fix logic as suggested
mrazauskas Apr 4, 2022
0c8402c
add migration guide
mrazauskas Apr 5, 2022
07ef70c
Revert "add migration guide"
SimenB Apr 5, 2022
e9e5cfa
Merge branch 'main' into useFakeTimers-bag-of-options
SimenB Apr 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -23,6 +23,8 @@
- `[jest-environment-node]` [**BREAKING**] Second argument `context` to constructor is mandatory ([#12469](https://github.com/facebook/jest/pull/12469))
- `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418))
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[@jest/fake-timers]` [**BREAKING**] Rename `timers` configuration option to `fakeTimers` ([#12572](https://github.com/facebook/jest/pull/12572))
- `[@jest/fake-timers]` [**BREAKING**] Allow `jest.useFakeTimers()` and `projectConfig.fakeTimers` to take an options bag ([#12572](https://github.com/facebook/jest/pull/12572))
- `[jest-haste-map]` [**BREAKING**] `HasteMap.create` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008))
- `[jest-haste-map]` Add support for `dependencyExtractor` written in ESM ([#12008](https://github.com/facebook/jest/pull/12008))
- `[jest-mock]` [**BREAKING**] Rename exported utility types `ClassLike`, `FunctionLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435), [#12489](https://github.com/facebook/jest/pull/12489))
Expand Down
4 changes: 4 additions & 0 deletions docs/CLI.md
Expand Up @@ -202,6 +202,10 @@ Make calling deprecated APIs throw helpful error messages. Useful for easing the

Alias: `-e`. Use this flag to show full diffs and errors instead of a patch.

### `--fakeTimers=<json string>`
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

A JSON string with configuration options for fake timers. For details see [Fake Timers API](JestObjectAPI.md#fake-timers) documentation.

### `--filter=<file>`

Path to a module exporting a filtering function. This method receives a list of tests which can be manipulated to exclude tests from running. Especially useful when used in conjunction with a testing infrastructure to filter known broken.
Expand Down
77 changes: 69 additions & 8 deletions docs/Configuration.md
Expand Up @@ -395,6 +395,75 @@ Jest's ESM support is still experimental, see [its docs for more details](ECMASc
}
```

### `fakeTimers` \[object]
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

Default: `{}`

This option allows configuration of the implementation of fake timers. The fake timers may be useful when a piece of code sets a long timeout that we don't want to wait for in a test. For more details see [Fake Timers API](JestObjectAPI.md#fake-timers) documentation.

Here is how you can enable fake timers globally for all tests:

```json
{
"fakeTimers": {
"enableGlobally": true
}
}
```

Configuration options:

```ts
type FakeableAPI =
| 'Date'
| 'hrtime'
| 'nextTick'
| 'performance'
| 'queueMicrotask'
| 'requestAnimationFrame'
| 'cancelAnimationFrame'
| 'requestIdleCallback'
| 'cancelIdleCallback'
| 'setImmediate'
| 'clearImmediate'
| 'setInterval'
| 'clearInterval'
| 'setTimeout'
| 'clearTimeout';

type ModernFakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
*/
advanceTimers?: boolean | number;
/** List of names of APIs that should not be faked. The default is `[]`, meaning all APIs are faked. */
doNotFake?: Array<FakeableAPI>;
/** Whether fake timers should be enabled for all test files. The default is `false`. */
enableGlobally?: boolean;
/** Sets current system time to be used by fake timers. The default is `Date.now()`. */
now?: number;
/** Maximum number of timers that will be run. The default is `100_000` timers. */
timerLimit?: number;
};
```

:::info Legacy Fake Timers

For some reason you might have to use legacy implementation of fake timers. Here is how to enable it globally (additional options are not supported):

```json
{
"fakeTimers": {
"enableGlobally": true,
"legacyFakeTimers": true
}
}
```

:::

### `forceCoverageMatch` \[array&lt;string&gt;]

Default: `['']`
Expand Down Expand Up @@ -1458,14 +1527,6 @@ Default: `5000`

Default timeout of a test in milliseconds.

### `timers` \[string]

Default: `real`

Setting this value to `fake` or `modern` enables fake timers for all tests by default. Fake timers are useful when a piece of code sets a long timeout that we don't want to wait for in a test. You can learn more about fake timers [here](JestObjectAPI.md#jestusefaketimersimplementation-modern--legacy).

If the value is `legacy`, the old implementation will be used as implementation instead of one backed by [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers).

### `transform` \[object&lt;string, pathToTransformer | \[pathToTransformer, object]&gt;]

Default: `{"\\.[jt]sx?$": "babel-jest"}`
Expand Down
119 changes: 111 additions & 8 deletions docs/JestObjectAPI.md
Expand Up @@ -628,19 +628,110 @@ test('direct', () => {
});
```

## Mock Timers
## Fake Timers

### `jest.useFakeTimers(implementation?: 'modern' | 'legacy')`
### `jest.useFakeTimers(fakeTimersConfig?)`

Instructs Jest to use fake versions of the standard timer functions (`setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `nextTick`, `setImmediate` and `clearImmediate` as well as `Date`).
Instructs Jest to use fake versions of the global date, performance, time and timer APIs. Fake timers implementation is backed by [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers).

If you pass `'legacy'` as an argument, Jest's legacy implementation will be used rather than one based on [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers).
Fake timers will swap out `Date`, `performance.now()`, `queueMicrotask()`, `setImmediate()`, `clearImmediate()`, `setInterval()`, `clearInterval()`, `setTimeout()`, `clearTimeout()` with an implementation that gets its time from the fake clock.

In Node environment `process.hrtime`, `process.nextTick()` and in JSDOM environment `requestAnimationFrame()`, `cancelAnimationFrame()`, `requestIdleCallback()`, `cancelIdleCallback()` will be replaced as well.

Configuration options:

```ts
type FakeableAPI =
| 'Date'
| 'hrtime'
| 'nextTick'
| 'performance'
| 'queueMicrotask'
| 'requestAnimationFrame'
| 'cancelAnimationFrame'
| 'requestIdleCallback'
| 'cancelIdleCallback'
| 'setImmediate'
| 'clearImmediate'
| 'setInterval'
| 'clearInterval'
| 'setTimeout'
| 'clearTimeout';

type FakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
*/
advanceTimers?: boolean | number;
/** List of names of APIs that should not be faked. The default is `[]`. */
doNotFake?: Array<FakeableAPI>;
/** Sets current system time to be used by fake timers. The default is `Date.now()`. */
now?: number | Date;
/**
* The maximum number of timers that will be run when calling `jest.runAllTimers()`.
* The default is `100_000` timers.
*/
timerLimit?: number;
};
```

Calling `jest.useRealTimers()` will use fake timers for all tests within the file, until original timers are restored with `jest.useRealTimers()`.
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

You can call `jest.useFakeTimers()` or `jest.useRealTimers()` from anywhere: top level, inside an `test` block, etc. Keep in mind that this is a **global operation** and will affect other tests within the same file. Calling `jest.useFakeTimers()` once again in the same test file would reset the internal state (e.g. timer count) and reinstall fake timers using the provided options:

```js
test('advance the timers automatically', () => {
jest.useFakeTimers({advanceTimers: true});
// ...
});

test('do not advance the timers and do not fake `performance`', () => {
jest.useFakeTimers({doNotFake: ['performance']});
// ...
});

test('uninstall fake timers for the rest of tests in the file', () => {
jest.useRealTimers();
// ...
});
```

:::info Legacy Fake Timers

For some reason you might have to use legacy implementation of fake timers. It can be enabled like this (additional options are not supported):

```js
jest.useFakeTimers({
legacyFakeTimers: true,
});
```

Legacy fake timers will swap out `setImmediate()`, `clearImmediate()`, `setInterval()`, `clearInterval()`, `setTimeout()`, `clearTimeout()` with Jest [mock functions](MockFunctionAPI.md). In Node environment `process.nextTick()` and in JSDOM environment `requestAnimationFrame()`, `cancelAnimationFrame()` will be also replaced.

:::

Returns the `jest` object for chaining.

### `jest.useRealTimers()`

Instructs Jest to use the real versions of the standard timer functions.
Instructs Jest to restore the original implementations of the global date, performance, time and timer APIs. For example, you may call `jest.useRealTimers()` inside `afterEach` hook to restore timers after each test:

```js
afterEach(() => {
jest.useRealTimers();
});

test('do something with fake timers', () => {
jest.useFakeTimers();
// ...
});

test('do something with real timers', () => {
// ...
});
```

Returns the `jest` object for chaining.

Expand All @@ -662,7 +753,11 @@ This is often useful for synchronously executing setTimeouts during a test in or

Exhausts all tasks queued by `setImmediate()`.

> Note: This function is not available when using modern fake timers implementation
:::info

This function is only available when using legacy fake timers implementation.

:::

### `jest.advanceTimersByTime(msToRun)`

Expand Down Expand Up @@ -696,13 +791,21 @@ Returns the number of fake timers still left to run.

Set the current system time used by fake timers. Simulates a user changing the system clock while your program is running. It affects the current time but it does not in itself cause e.g. timers to fire; they will fire exactly as they would have done without the call to `jest.setSystemTime()`.

> Note: This function is only available when using modern fake timers implementation
:::info

This function is not available when using legacy fake timers implementation.

:::

### `jest.getRealSystemTime()`

When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function.

> Note: This function is only available when using modern fake timers implementation
:::info

This function is not available when using legacy fake timers implementation.

:::

## Misc

Expand Down
80 changes: 41 additions & 39 deletions docs/TimerMocks.md
Expand Up @@ -3,11 +3,19 @@ id: timer-mocks
title: Timer Mocks
---

The native timer functions (i.e., `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time. [Great Scott!](https://www.youtube.com/watch?v=QZoJ2Pt27BY)
The native timer functions (i.e., `setTimeout()`, `setInterval()`, `clearTimeout()`, `clearInterval()`) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time. [Great Scott!](https://www.youtube.com/watch?v=QZoJ2Pt27BY)

```javascript title="timerGame.js"
'use strict';
:::info

Also see [Fake Timers API](JestObjectAPI.md#fake-timers) documentation.

:::

## Enable Fake Timers

In the following example we enable fake timers by calling `jest.useFakeTimers()`. This is replacing the original implementation of `setTimeout()` and other timer functions. Timers can be restored to their normal behavior with `jest.useRealTimers()`.

```javascript title="timerGame.js"
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
Expand All @@ -20,8 +28,6 @@ module.exports = timerGame;
```

```javascript title="__tests__/timerGame-test.js"
'use strict';

jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

Expand All @@ -34,27 +40,6 @@ test('waits 1 second before ending the game', () => {
});
```

Here we enable fake timers by calling `jest.useFakeTimers()`. This mocks out `setTimeout` and other timer functions with mock functions. Timers can be restored to their normal behavior with `jest.useRealTimers()`.

While you can call `jest.useFakeTimers()` or `jest.useRealTimers()` from anywhere (top level, inside an `it` block, etc.), it is a **global operation** and will affect other tests within the same file. Additionally, you need to call `jest.useFakeTimers()` to reset internal counters before each test. If you plan to not use fake timers in all your tests, you will want to clean up manually, as otherwise the faked timers will leak across tests:
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

```javascript
afterEach(() => {
jest.useRealTimers();
});

test('do something with fake timers', () => {
jest.useFakeTimers();
// ...
});

test('do something with real timers', () => {
// ...
});
```

Currently, two implementations of the fake timers are included - `modern` and `legacy`, where `modern` is the default one. See [configuration](Configuration.md#timers-string) for how to configure it.

## Run All Timers

Another test we might want to write for this module is one that asserts that the callback is called after 1 second. To do this, we're going to use Jest's timer control APIs to fast-forward time right in the middle of the test:
Expand All @@ -81,17 +66,11 @@ test('calls the callback after 1 second', () => {

## Run Pending Timers

There are also scenarios where you might have a recursive timer -- that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop, throwing the following error:
There are also scenarios where you might have a recursive timer that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop, throwing the following error: "Aborting after running 100000 timers, assuming an infinite loop!"

```
Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...
```

So something like `jest.runAllTimers()` is not desirable. For these cases you might use `jest.runOnlyPendingTimers()`:
If that is your case, using `jest.runOnlyPendingTimers()` will solve the problem:

```javascript title="infiniteTimerGame.js"
'use strict';

function infiniteTimerGame(callback) {
console.log('Ready....go!');

Expand All @@ -110,8 +89,6 @@ module.exports = infiniteTimerGame;
```

```javascript title="__tests__/infiniteTimerGame-test.js"
'use strict';

jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

Expand Down Expand Up @@ -142,13 +119,21 @@ describe('infiniteTimerGame', () => {
});
```

:::note

For debugging or any other reason you can change the limit of timers that will be run before throwing an error:

```js
jest.useFakeTimers({timerLimit: 100});
```

:::

## Advance Timers by Time

Another possibility is use `jest.advanceTimersByTime(msToRun)`. When this API is called, all timers are advanced by `msToRun` milliseconds. All pending "macro-tasks" that have been queued via setTimeout() or setInterval(), and would be executed during this time frame, will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue that should be run within msToRun milliseconds.

```javascript title="timerGame.js"
'use strict';

function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
Expand Down Expand Up @@ -182,4 +167,21 @@ it('calls the callback after 1 second via advanceTimersByTime', () => {

Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have `jest.clearAllTimers()`.

The code for this example is available at [examples/timer](https://github.com/facebook/jest/tree/main/examples/timer).
## Selective Faking

Sometimes your code may require to avoid overwriting the original implementation of one or another API. If that is the case, you can use `doNotFake` option. For example, here is how you could provide a custom mock function for `performance.mark()` in jsdom environment:

```js
/**
* @jest-environment jsdom
*/

const mockPerformanceMark = jest.fn();
window.performance.mark = mockPerformanceMark;

test('allows mocking `performance.mark()`', () => {
jest.useFakeTimers({doNotFake: ['performance']});

expect(window.performance.mark).toBe(mockPerformanceMark);
});
```