Skip to content

Commit 92909f6

Browse files
cjihrigdanielleadams
authored andcommitted
test_runner: support function mocking
This commit allows tests in the test runner to mock functions and methods. PR-URL: #45326 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 9ac7df1 commit 92909f6

File tree

6 files changed

+1483
-2
lines changed

6 files changed

+1483
-2
lines changed

doc/api/test.md

+360
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,89 @@ Otherwise, the test is considered to be a failure. Test files must be
352352
executable by Node.js, but are not required to use the `node:test` module
353353
internally.
354354

355+
## Mocking
356+
357+
The `node:test` module supports mocking during testing via a top-level `mock`
358+
object. The following example creates a spy on a function that adds two numbers
359+
together. The spy is then used to assert that the function was called as
360+
expected.
361+
362+
```mjs
363+
import assert from 'node:assert';
364+
import { mock, test } from 'node:test';
365+
366+
test('spies on a function', () => {
367+
const sum = mock.fn((a, b) => {
368+
return a + b;
369+
});
370+
371+
assert.strictEqual(sum.mock.calls.length, 0);
372+
assert.strictEqual(sum(3, 4), 7);
373+
assert.strictEqual(sum.mock.calls.length, 1);
374+
375+
const call = sum.mock.calls[0];
376+
assert.deepStrictEqual(call.arguments, [3, 4]);
377+
assert.strictEqual(call.result, 7);
378+
assert.strictEqual(call.error, undefined);
379+
380+
// Reset the globally tracked mocks.
381+
mock.reset();
382+
});
383+
```
384+
385+
```cjs
386+
'use strict';
387+
const assert = require('node:assert');
388+
const { mock, test } = require('node:test');
389+
390+
test('spies on a function', () => {
391+
const sum = mock.fn((a, b) => {
392+
return a + b;
393+
});
394+
395+
assert.strictEqual(sum.mock.calls.length, 0);
396+
assert.strictEqual(sum(3, 4), 7);
397+
assert.strictEqual(sum.mock.calls.length, 1);
398+
399+
const call = sum.mock.calls[0];
400+
assert.deepStrictEqual(call.arguments, [3, 4]);
401+
assert.strictEqual(call.result, 7);
402+
assert.strictEqual(call.error, undefined);
403+
404+
// Reset the globally tracked mocks.
405+
mock.reset();
406+
});
407+
```
408+
409+
The same mocking functionality is also exposed on the [`TestContext`][] object
410+
of each test. The following example creates a spy on an object method using the
411+
API exposed on the `TestContext`. The benefit of mocking via the test context is
412+
that the test runner will automatically restore all mocked functionality once
413+
the test finishes.
414+
415+
```js
416+
test('spies on an object method', (t) => {
417+
const number = {
418+
value: 5,
419+
add(a) {
420+
return this.value + a;
421+
},
422+
};
423+
424+
t.mock.method(number, 'add');
425+
assert.strictEqual(number.add.mock.calls.length, 0);
426+
assert.strictEqual(number.add(3), 8);
427+
assert.strictEqual(number.add.mock.calls.length, 1);
428+
429+
const call = number.add.mock.calls[0];
430+
431+
assert.deepStrictEqual(call.arguments, [3]);
432+
assert.strictEqual(call.result, 8);
433+
assert.strictEqual(call.target, undefined);
434+
assert.strictEqual(call.this, number);
435+
});
436+
```
437+
355438
## `run([options])`
356439

357440
<!-- YAML
@@ -630,6 +713,281 @@ describe('tests', async () => {
630713
});
631714
```
632715

716+
## Class: `MockFunctionContext`
717+
718+
<!-- YAML
719+
added: REPLACEME
720+
-->
721+
722+
The `MockFunctionContext` class is used to inspect or manipulate the behavior of
723+
mocks created via the [`MockTracker`][] APIs.
724+
725+
### `ctx.calls`
726+
727+
<!-- YAML
728+
added: REPLACEME
729+
-->
730+
731+
* {Array}
732+
733+
A getter that returns a copy of the internal array used to track calls to the
734+
mock. Each entry in the array is an object with the following properties.
735+
736+
* `arguments` {Array} An array of the arguments passed to the mock function.
737+
* `error` {any} If the mocked function threw then this property contains the
738+
thrown value. **Default:** `undefined`.
739+
* `result` {any} The value returned by the mocked function.
740+
* `stack` {Error} An `Error` object whose stack can be used to determine the
741+
callsite of the mocked function invocation.
742+
* `target` {Function|undefined} If the mocked function is a constructor, this
743+
field contains the class being constructed. Otherwise this will be
744+
`undefined`.
745+
* `this` {any} The mocked function's `this` value.
746+
747+
### `ctx.callCount()`
748+
749+
<!-- YAML
750+
added: REPLACEME
751+
-->
752+
753+
* Returns: {integer} The number of times that this mock has been invoked.
754+
755+
This function returns the number of times that this mock has been invoked. This
756+
function is more efficient than checking `ctx.calls.length` because `ctx.calls`
757+
is a getter that creates a copy of the internal call tracking array.
758+
759+
### `ctx.mockImplementation(implementation)`
760+
761+
<!-- YAML
762+
added: REPLACEME
763+
-->
764+
765+
* `implementation` {Function|AsyncFunction} The function to be used as the
766+
mock's new implementation.
767+
768+
This function is used to change the behavior of an existing mock.
769+
770+
The following example creates a mock function using `t.mock.fn()`, calls the
771+
mock function, and then changes the mock implementation to a different function.
772+
773+
```js
774+
test('changes a mock behavior', (t) => {
775+
let cnt = 0;
776+
777+
function addOne() {
778+
cnt++;
779+
return cnt;
780+
}
781+
782+
function addTwo() {
783+
cnt += 2;
784+
return cnt;
785+
}
786+
787+
const fn = t.mock.fn(addOne);
788+
789+
assert.strictEqual(fn(), 1);
790+
fn.mock.mockImplementation(addTwo);
791+
assert.strictEqual(fn(), 3);
792+
assert.strictEqual(fn(), 5);
793+
});
794+
```
795+
796+
### `ctx.mockImplementationOnce(implementation[, onCall])`
797+
798+
<!-- YAML
799+
added: REPLACEME
800+
-->
801+
802+
* `implementation` {Function|AsyncFunction} The function to be used as the
803+
mock's implementation for the invocation number specified by `onCall`.
804+
* `onCall` {integer} The invocation number that will use `implementation`. If
805+
the specified invocation has already occurred then an exception is thrown.
806+
**Default:** The number of the next invocation.
807+
808+
This function is used to change the behavior of an existing mock for a single
809+
invocation. Once invocation `onCall` has occurred, the mock will revert to
810+
whatever behavior it would have used had `mockImplementationOnce()` not been
811+
called.
812+
813+
The following example creates a mock function using `t.mock.fn()`, calls the
814+
mock function, changes the mock implementation to a different function for the
815+
next invocation, and then resumes its previous behavior.
816+
817+
```js
818+
test('changes a mock behavior once', (t) => {
819+
let cnt = 0;
820+
821+
function addOne() {
822+
cnt++;
823+
return cnt;
824+
}
825+
826+
function addTwo() {
827+
cnt += 2;
828+
return cnt;
829+
}
830+
831+
const fn = t.mock.fn(addOne);
832+
833+
assert.strictEqual(fn(), 1);
834+
fn.mock.mockImplementationOnce(addTwo);
835+
assert.strictEqual(fn(), 3);
836+
assert.strictEqual(fn(), 4);
837+
});
838+
```
839+
840+
### `ctx.restore()`
841+
842+
<!-- YAML
843+
added: REPLACEME
844+
-->
845+
846+
Resets the implementation of the mock function to its original behavior. The
847+
mock can still be used after calling this function.
848+
849+
## Class: `MockTracker`
850+
851+
<!-- YAML
852+
added: REPLACEME
853+
-->
854+
855+
The `MockTracker` class is used to manage mocking functionality. The test runner
856+
module provides a top level `mock` export which is a `MockTracker` instance.
857+
Each test also provides its own `MockTracker` instance via the test context's
858+
`mock` property.
859+
860+
### `mock.fn([original[, implementation]][, options])`
861+
862+
<!-- YAML
863+
added: REPLACEME
864+
-->
865+
866+
* `original` {Function|AsyncFunction} An optional function to create a mock on.
867+
**Default:** A no-op function.
868+
* `implementation` {Function|AsyncFunction} An optional function used as the
869+
mock implementation for `original`. This is useful for creating mocks that
870+
exhibit one behavior for a specified number of calls and then restore the
871+
behavior of `original`. **Default:** The function specified by `original`.
872+
* `options` {Object} Optional configuration options for the mock function. The
873+
following properties are supported:
874+
* `times` {integer} The number of times that the mock will use the behavior of
875+
`implementation`. Once the mock function has been called `times` times, it
876+
will automatically restore the behavior of `original`. This value must be an
877+
integer greater than zero. **Default:** `Infinity`.
878+
* Returns: {Proxy} The mocked function. The mocked function contains a special
879+
`mock` property, which is an instance of [`MockFunctionContext`][], and can
880+
be used for inspecting and changing the behavior of the mocked function.
881+
882+
This function is used to create a mock function.
883+
884+
The following example creates a mock function that increments a counter by one
885+
on each invocation. The `times` option is used to modify the mock behavior such
886+
that the first two invocations add two to the counter instead of one.
887+
888+
```js
889+
test('mocks a counting function', (t) => {
890+
let cnt = 0;
891+
892+
function addOne() {
893+
cnt++;
894+
return cnt;
895+
}
896+
897+
function addTwo() {
898+
cnt += 2;
899+
return cnt;
900+
}
901+
902+
const fn = t.mock.fn(addOne, addTwo, { times: 2 });
903+
904+
assert.strictEqual(fn(), 2);
905+
assert.strictEqual(fn(), 4);
906+
assert.strictEqual(fn(), 5);
907+
assert.strictEqual(fn(), 6);
908+
});
909+
```
910+
911+
### `mock.method(object, methodName[, implementation][, options])`
912+
913+
<!-- YAML
914+
added: REPLACEME
915+
-->
916+
917+
* `object` {Object} The object whose method is being mocked.
918+
* `methodName` {string|symbol} The identifier of the method on `object` to mock.
919+
If `object[methodName]` is not a function, an error is thrown.
920+
* `implementation` {Function|AsyncFunction} An optional function used as the
921+
mock implementation for `object[methodName]`. **Default:** The original method
922+
specified by `object[methodName]`.
923+
* `options` {Object} Optional configuration options for the mock method. The
924+
following properties are supported:
925+
* `getter` {boolean} If `true`, `object[methodName]` is treated as a getter.
926+
This option cannot be used with the `setter` option. **Default:** false.
927+
* `setter` {boolean} If `true`, `object[methodName]` is treated as a setter.
928+
This option cannot be used with the `getter` option. **Default:** false.
929+
* `times` {integer} The number of times that the mock will use the behavior of
930+
`implementation`. Once the mocked method has been called `times` times, it
931+
will automatically restore the original behavior. This value must be an
932+
integer greater than zero. **Default:** `Infinity`.
933+
* Returns: {Proxy} The mocked method. The mocked method contains a special
934+
`mock` property, which is an instance of [`MockFunctionContext`][], and can
935+
be used for inspecting and changing the behavior of the mocked method.
936+
937+
This function is used to create a mock on an existing object method. The
938+
following example demonstrates how a mock is created on an existing object
939+
method.
940+
941+
```js
942+
test('spies on an object method', (t) => {
943+
const number = {
944+
value: 5,
945+
subtract(a) {
946+
return this.value - a;
947+
},
948+
};
949+
950+
t.mock.method(number, 'subtract');
951+
assert.strictEqual(number.subtract.mock.calls.length, 0);
952+
assert.strictEqual(number.subtract(3), 2);
953+
assert.strictEqual(number.subtract.mock.calls.length, 1);
954+
955+
const call = number.subtract.mock.calls[0];
956+
957+
assert.deepStrictEqual(call.arguments, [3]);
958+
assert.strictEqual(call.result, 2);
959+
assert.strictEqual(call.error, undefined);
960+
assert.strictEqual(call.target, undefined);
961+
assert.strictEqual(call.this, number);
962+
});
963+
```
964+
965+
### `mock.reset()`
966+
967+
<!-- YAML
968+
added: REPLACEME
969+
-->
970+
971+
This function restores the default behavior of all mocks that were previously
972+
created by this `MockTracker` and disassociates the mocks from the
973+
`MockTracker` instance. Once disassociated, the mocks can still be used, but the
974+
`MockTracker` instance can no longer be used to reset their behavior or
975+
otherwise interact with them.
976+
977+
After each test completes, this function is called on the test context's
978+
`MockTracker`. If the global `MockTracker` is used extensively, calling this
979+
function manually is recommended.
980+
981+
### `mock.restoreAll()`
982+
983+
<!-- YAML
984+
added: REPLACEME
985+
-->
986+
987+
This function restores the default behavior of all mocks that were previously
988+
created by this `MockTracker`. Unlike `mock.reset()`, `mock.restoreAll()` does
989+
not disassociate the mocks from the `MockTracker` instance.
990+
633991
## Class: `TapStream`
634992

635993
<!-- YAML
@@ -935,6 +1293,8 @@ added: v18.7.0
9351293
[`--test-name-pattern`]: cli.md#--test-name-pattern
9361294
[`--test-only`]: cli.md#--test-only
9371295
[`--test`]: cli.md#--test
1296+
[`MockFunctionContext`]: #class-mockfunctioncontext
1297+
[`MockTracker`]: #class-mocktracker
9381298
[`SuiteContext`]: #class-suitecontext
9391299
[`TestContext`]: #class-testcontext
9401300
[`context.diagnostic`]: #contextdiagnosticmessage

0 commit comments

Comments
 (0)