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

test_runner: introduces a new MockTimers API #47775

Merged
Merged
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift click to select a range
568ad96
test_runner: add initial draft for fakeTimers
ErickWendel Apr 28, 2023
ac0fab3
test_runner: move fakerTimers to mock module
ErickWendel May 1, 2023
e54a162
test_runner: change fakeTimers prop name
ErickWendel May 1, 2023
71ffd10
test_runner: implement PriorityQueue for timers
ErickWendel May 1, 2023
a0f8afd
test_runner: add @ljharb suggestion
ErickWendel May 1, 2023
0f55f65
test_runner: add @benjamingr suggestion
ErickWendel May 1, 2023
e34a426
test_runner: add fake setTimeout initial impl
ErickWendel May 1, 2023
df35174
test_runner: add @molow suggestion
ErickWendel May 1, 2023
e5af52f
test_runner: fix a test
ErickWendel May 1, 2023
a22321f
test_runner: rename from fakeTimers to mockTimers
ErickWendel May 3, 2023
21be0bc
test_runner: add experimental warning
ErickWendel May 3, 2023
102a830
test_runner: fix tests
ErickWendel May 3, 2023
1d30e47
test_runner: add @ljharb suggestion
ErickWendel May 3, 2023
0cb4e7c
test_runner: fix possible truthy evaluation on tick by @ljharb
ErickWendel May 3, 2023
a53aedc
test_runner: add tests for clearTimeout
ErickWendel May 3, 2023
1a2cf34
test_runner: add tests for setInterval modules
ErickWendel May 3, 2023
db2a79f
test_runner: add tests for clearInterval modules
ErickWendel May 3, 2023
f200467
test_runner: add impl for timers.promises.setInterval function
ErickWendel May 4, 2023
cc652fe
test_runner: fix setInterval algorithm
ErickWendel May 4, 2023
942f504
test_runner: add abortController support for promises.setTimeout and …
ErickWendel May 14, 2023
bb75287
test_runner: add test cases for abortController and remove listeners …
ErickWendel May 14, 2023
b0baf00
test_runner: change import order
ErickWendel May 15, 2023
92bf115
test_runner: add ArrayPrototypeAt instead of [].at
ErickWendel May 15, 2023
1fe731e
test_runner: return safe asyncIterator
ErickWendel May 15, 2023
08f27ba
test_runner: make promises.setInterval update time during iterations
ErickWendel May 15, 2023
3e39782
test_runner: add __proto__ null while returning the object
ErickWendel May 19, 2023
a563ce9
test_runner: make promises.setInterval work with abortControllers
ErickWendel May 30, 2023
17381f1
test_runner: rm unsafe array iteration and comments
ErickWendel May 30, 2023
3677482
test_runner: avoid avoid code repetition
ErickWendel May 30, 2023
1825426
test_runner: (rollback) avoid avoid code repetition
ErickWendel May 30, 2023
abc7c96
test_runner: rm unecessarly PromiseRejection
ErickWendel May 30, 2023
87b0d36
test_runner: add timers.reset on test postRun
ErickWendel Jun 5, 2023
a001e65
test_runner: add config for choosing timers to use, error handling an…
ErickWendel Jun 5, 2023
d168614
test_runner: implement releaseAllTimers function
ErickWendel Jun 5, 2023
1e09a22
test_runner: reach 100% code coverage
ErickWendel Jun 5, 2023
89b489b
test_runner: rename function to runAll
ErickWendel Jun 6, 2023
2b64a4a
fix flaky test
ErickWendel Jun 8, 2023
a9ed28d
add abort once flag
ErickWendel Jun 8, 2023
b383a1c
check that timeout will only be called when reaches the specified tim…
ErickWendel Jun 8, 2023
ba725f4
fix lint problems
ErickWendel Jun 8, 2023
1bcbe65
test_runner: add initial doc
ErickWendel Jun 12, 2023
7c65e64
test_runner: fix heading
ErickWendel Jun 12, 2023
aaf7070
test_runner: fix text
ErickWendel Jun 12, 2023
ac9f5b6
test_runner: fix links
ErickWendel Jun 12, 2023
13fab2b
test_runner: add docs for timers.tick
ErickWendel Jun 12, 2023
296f5b0
test_runner: add docs for timers.runAll
ErickWendel Jun 12, 2023
a75ddb4
doc rm empty spaces
ErickWendel Jun 12, 2023
1bee321
Update test/parallel/test-runner-mock-timers.js
ErickWendel Jun 13, 2023
bc8fea7
Update lib/internal/test_runner/mock/mock_timers.js
ErickWendel Jun 13, 2023
1bfd7b8
format test.md
ErickWendel Jun 13, 2023
d2c9a1a
add 1ms to tick to fix flakyness
ErickWendel Jun 13, 2023
93950f5
test_runner: add 50ms to .tick so setInterval will not be flaky
ErickWendel Jun 13, 2023
49e8eba
test_runner: revert last commit
ErickWendel Jun 13, 2023
4d3a45c
founde the bug 🐛
MoLow Jun 13, 2023
12772d1
Apply suggestions from code review
ErickWendel Jun 14, 2023
5c9e425
apply suggestions from review
ErickWendel Jun 14, 2023
0424bf5
add info about support for destructuring from modules
ErickWendel Jun 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test_runner: add docs for timers.tick
Signed-off-by: Erick Wendel <[email protected]>
  • Loading branch information
ErickWendel committed Jun 12, 2023
commit 13fab2bb1030b043db91a8516716d6c0d48f7bc5
339 changes: 339 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 1506,345 @@ added:

> Stability: 1 - Experimental

Mocking timers is a technique commonly used in software testing to simulate and
control the behavior of timers, such as `setInterval` and `setTimeout`,
without actually waiting for the specified time intervals.

The [`MockTracker`][] provides a top level `timers` export
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved
which is a MockTimers instance
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved

### `timers.enable([,timers])`
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved
<!-- YAML
added:
- REPLACEME
-->

Enables timer mocking for the specified timers.

* `timers` {Array} An optional array containing the timers to mock.
The currently supported timer values are `'setInterval'` and `'setTimeout'`.
If no array is provided, all timers (`'setInterval'`, `'clearInterval'`, `'setTimeout'`,
and `'clearTimeout'`) will be mocked by default.

**Note:** When you enable mocking for a specific timer, its associated
clear function will also be implicitly mocked.

Example usage:

```mjs
import { mock } from 'node:test';
mock.timers.enable(['setInterval']);
```
```js
const { mock } = require('node:test');
mock.timers.enable(['setInterval']);
```

The above example enables mocking for the `setInterval` timer and
implicitly mocks the `clearInterval` function. Only the `setInterval`
and `clearInterval` functions from [node:timers](./timers.md),
[node:timers/promises](./timers.md#timers-promises-api), and
`globalThis` will be mocked.

Alternatively, if you call `mock.timers.enable()` without any parameters:

All timers (`'setInterval'`, `'clearInterval'`, `'setTimeout'`, and `'clearTimeout'`)
will be mocked. The `setInterval`, `clearInterval`, `setTimeout`, and `clearTimeout`
functions from `node:timers`, `node:timers/promises`,
and `globalThis` will be mocked.

### `timers.reset()`
<!-- YAML
added:
- REPLACEME
-->

This function restores the default behavior of all mocks that were previously
created by this `MockTimers` instance and disassociates the mocks
from the `MockTracker` instance.

**Note:** After each test completes, this function is called on
the test context's `MockTracker`.

```mjs
import { mock } from 'node:test';
mock.timers.reset();
```
```js
const { mock } = require('node:test');
mock.timers.reset();
```

### `timers.tick(milliseconds)`
<!-- YAML
added:
- REPLACEME
-->
Advances time for all mocked timers.

* `milliseconds` {number} The amount of time, in milliseconds,
to advance the timers.

**Note:** This diverges from how `setTimeout` in Node.js behaves and accepts
only positive numbers. In Node.js, `setTimeout` with negative numbers is
only supported for web compatibility reasons.

The following example mocks a `setTimeout` function and
by using `.tick` advances in
time triggering all pending timers.

```mjs
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();

context.mock.timers.enable(['setTimeout']);

setTimeout(fn, 9999);

assert.strictEqual(fn.mock.callCount(), 0);

// Advance in time
context.mock.timers.tick(9999);

assert.strictEqual(fn.mock.callCount(), 1);
});
```
```js
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);

setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);

// Advance in time
context.mock.timers.tick(9999);

assert.strictEqual(fn.mock.callCount(), 1);
});
```
Alternativelly, the `.tick` function can be called many times

```mjs
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);
const nineSecs = 9500;
setTimeout(fn, nineSecs);

const twoSeconds = 3000;
context.mock.timers.tick(twoSeconds);
context.mock.timers.tick(twoSeconds);
context.mock.timers.tick(twoSeconds);

assert.strictEqual(fn.mock.callCount(), 1);
});
```

```js
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();
context.mock.timers.enable(['setTimeout']);
const nineSecs = 9500;
setTimeout(fn, nineSecs);

const twoSeconds = 3000;
context.mock.timers.tick(twoSeconds);
context.mock.timers.tick(twoSeconds);
context.mock.timers.tick(twoSeconds);

assert.strictEqual(fn.mock.callCount(), 1);
});
```

#### Using Clear functions
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved

As mentioned, all clear functions from timers (clearTimeout and clearInterval)
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved
are implicity mocked.Take a look at this example using `setTimeout`:
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved

```mjs
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();

// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
const id = setTimeout(fn, 9999);

// Implicity mocked as well
clearTimeout(id);
context.mock.timers.tick(9999);

// As that setTimeout was cleared the mock function will never be called
assert.strictEqual(fn.mock.callCount(), 0);
});
```

```js
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
const fn = context.mock.fn();

// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
const id = setTimeout(fn, 9999);

// Implicity mocked as well
clearTimeout(id);
context.mock.timers.tick(9999);

// As that setTimeout was cleared the mock function will never be called
assert.strictEqual(fn.mock.callCount(), 0);
});
```
#### Working with Node.js timers modules

Once you enable mocking timers, [node:timers](./timers.md),
[node:timers/promises](./timers.md#timers-promises-api) modules,
and timers from the Node.js global context are enabled:

```mjs
import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimers from 'node:timers';
import nodeTimersPromises from 'node:timers/promises';
ErickWendel marked this conversation as resolved.
Show resolved Hide resolved

test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => {
const globalTimeoutObjectSpy = context.mock.fn();
const nodeTimerSpy = context.mock.fn();
const nodeTimerPromiseSpy = context.mock.fn();

// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
setTimeout(globalTimeoutObjectSpy, 9999);
nodeTimers.setTimeout(nodeTimerSpy, 9999);

const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy);

// Advance in time
context.mock.timers.tick(9999);
assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1);
assert.strictEqual(nodeTimerSpy.mock.callCount(), 1);
await promise;
assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1);
});
```
```js
const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimers = require('node:timers');
const nodeTimersPromises = require('node:timers/promises');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => {
const globalTimeoutObjectSpy = context.mock.fn();
const nodeTimerSpy = context.mock.fn();
const nodeTimerPromiseSpy = context.mock.fn();

// Optionally choose what to mock
context.mock.timers.enable(['setTimeout']);
setTimeout(globalTimeoutObjectSpy, 9999);
nodeTimers.setTimeout(nodeTimerSpy, 9999);

const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy);

// Advance in time
context.mock.timers.tick(9999);
assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1);
assert.strictEqual(nodeTimerSpy.mock.callCount(), 1);
await promise;
assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1);
});
```
In Node.js, `setInterval` from [node:timers/promises](./timers.md#timers-promises-api)
is a Async Gerator and is also supported by this API:

```mjs
import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimersPromises from 'node:timers/promises';
test('should tick five times testing a real use case', async (context) => {

context.mock.timers.enable(['setInterval']);

const expectedIterations = 3;
const interval = 1000;
const startedAt = Date.now();
async function run() {
const times = [];
for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
times.push(time);
if (times.length === expectedIterations) break;
}
return times;
}

const r = run();
context.mock.timers.tick(interval);
context.mock.timers.tick(interval);
context.mock.timers.tick(interval);

const timeResults = await r;
assert.strictEqual(timeResults.length, expectedIterations);
for (let it = 1; it < expectedIterations; it ) {
assert.strictEqual(timeResults[it - 1], startedAt (interval * it));
}
});
```
```js
const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimersPromises = require('node:timers/promises');
test('should tick five times testing a real use case', async (context) => {

context.mock.timers.enable(['setInterval']);

const expectedIterations = 3;
const interval = 1000;
const startedAt = Date.now();
async function run() {
const times = [];
for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
times.push(time);
if (times.length === expectedIterations) break;
}
return times;
}

const r = run();
context.mock.timers.tick(interval);
context.mock.timers.tick(interval);
context.mock.timers.tick(interval);

const timeResults = await r;
assert.strictEqual(timeResults.length, expectedIterations);
for (let it = 1; it < expectedIterations; it ) {
assert.strictEqual(timeResults[it - 1], startedAt (interval * it));
}
});
```

### `timers.runAll()`
<!-- YAML
added:
- REPLACEME
-->

## Class: `TestsStream`

<!-- YAML
Expand Down