Skip to content

Commit 6158d74

Browse files
cjihrigRafaelGSS
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 <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 86e22b4 commit 6158d74

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
@@ -644,6 +727,281 @@ describe('tests', async () => {
644727
});
645728
```
646729

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

6491007
<!-- YAML
@@ -979,6 +1337,8 @@ added:
9791337
[`--test-name-pattern`]: cli.md#--test-name-pattern
9801338
[`--test-only`]: cli.md#--test-only
9811339
[`--test`]: cli.md#--test
1340+
[`MockFunctionContext`]: #class-mockfunctioncontext
1341+
[`MockTracker`]: #class-mocktracker
9821342
[`SuiteContext`]: #class-suitecontext
9831343
[`TestContext`]: #class-testcontext
9841344
[`context.diagnostic`]: #contextdiagnosticmessage

0 commit comments

Comments
 (0)