Skip to content

Commit 33e507d

Browse files
committed
test_runner: support function mocking
This commit allows tests in the test runner to mock functions and methods.
1 parent 7903f94 commit 33e507d

File tree

6 files changed

+1456
-2
lines changed

6 files changed

+1456
-2
lines changed

doc/api/test.md

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,62 @@ 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+
```js
363+
test('spies on a function', () => {
364+
const sum = test.mock.fn((a, b) => {
365+
return a + b;
366+
});
367+
368+
assert.strictEqual(sum.mock.calls.length, 0);
369+
assert.strictEqual(sum(3, 4), 7);
370+
assert.strictEqual(sum.mock.calls.length, 1);
371+
372+
const call = sum.mock.calls[0];
373+
assert.deepStrictEqual(call.arguments, [3, 4]);
374+
assert.strictEqual(call.result, 7);
375+
assert.strictEqual(call.error, undefined);
376+
377+
// Reset the globally tracked mocks.
378+
test.mock.reset();
379+
});
380+
```
381+
382+
The same mocking functionality is also exposed on the [`TestContext`][] object
383+
of each test. The following example creates a spy on an object method using the
384+
API exposed on the `TestContext`. The benefit of mocking via the test context is
385+
that the test runner will automatically restore all mocked functionality once
386+
the test finishes.
387+
388+
```js
389+
test('spies on an object method', (t) => {
390+
const number = {
391+
value: 5,
392+
add(a) {
393+
return this.value + a;
394+
},
395+
};
396+
397+
t.mock.method(number, 'add');
398+
assert.strictEqual(number.add.mock.calls.length, 0);
399+
assert.strictEqual(number.add(3), 8);
400+
assert.strictEqual(number.add.mock.calls.length, 1);
401+
402+
const call = number.add.mock.calls[0];
403+
404+
assert.deepStrictEqual(call.arguments, [3]);
405+
assert.strictEqual(call.result, 8);
406+
assert.strictEqual(call.target, undefined);
407+
assert.strictEqual(call.this, number);
408+
});
409+
```
410+
355411
## `run([options])`
356412

357413
<!-- YAML
@@ -644,6 +700,281 @@ describe('tests', async () => {
644700
});
645701
```
646702

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

649980
<!-- YAML
@@ -979,6 +1310,8 @@ added:
9791310
[`--test-name-pattern`]: cli.md#--test-name-pattern
9801311
[`--test-only`]: cli.md#--test-only
9811312
[`--test`]: cli.md#--test
1313+
[`MockFunctionContext`]: #class-mockfunctioncontext
1314+
[`MockTracker`]: #class-mocktracker
9821315
[`SuiteContext`]: #class-suitecontext
9831316
[`TestContext`]: #class-testcontext
9841317
[`context.diagnostic`]: #contextdiagnosticmessage

0 commit comments

Comments
 (0)