Skip to content

Commit 2e499ee

Browse files
cjihrigMoLow
authored andcommitted
feat: support function mocking
This commit allows tests in the test runner to mock functions and methods. PR-URL: nodejs/node#45326 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Matteo Collina <[email protected]> (cherry picked from commit 7c6682957b3c5f86d0616cebc0ad09cc2a1fd50d)
1 parent cff397a commit 2e499ee

File tree

7 files changed

+1511
-3
lines changed

7 files changed

+1511
-3
lines changed

README.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,78 @@ Otherwise, the test is considered to be a failure. Test files must be
359359
executable by Node.js, but are not required to use the `node:test` module
360360
internally.
361361

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

364436
<!-- YAML
@@ -611,6 +683,266 @@ describe('tests', async () => {
611683
});
612684
```
613685

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

616948
<!-- YAML
@@ -821,6 +1153,8 @@ The name of the suite.
8211153

8221154
[`AbortSignal`]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
8231155
[TAP]: https://testanything.org/
1156+
[`MockFunctionContext`]: #class-mockfunctioncontext
1157+
[`MockTracker`]: #class-mocktracke
8241158
[`SuiteContext`]: #class-suitecontext
8251159
[`TestContext`]: #class-testcontext
8261160
[`context.diagnostic`]: #contextdiagnosticmessage

lib/internal/per_context/primordials.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ exports.Error = Error
2222
exports.ErrorCaptureStackTrace = (...args) => Error.captureStackTrace(...args)
2323
exports.FunctionPrototype = Function.prototype
2424
exports.FunctionPrototypeBind = (fn, obj, ...args) => fn.bind(obj, ...args)
25+
exports.FunctionPrototypeCall = (fn, obj, ...args) => fn.call(obj, ...args)
2526
exports.MathMax = (...args) => Math.max(...args)
2627
exports.Number = Number
28+
exports.NumberIsInteger = Number.isInteger
29+
exports.NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
30+
exports.NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
2731
exports.ObjectAssign = (target, ...sources) => Object.assign(target, ...sources)
2832
exports.ObjectCreate = obj => Object.create(obj)
2933
exports.ObjectDefineProperties = (obj, props) => Object.defineProperties(obj, props)
@@ -40,6 +44,7 @@ exports.PromiseAll = iterator => Promise.all(iterator)
4044
exports.PromisePrototypeThen = (promise, thenFn, catchFn) => promise.then(thenFn, catchFn)
4145
exports.PromiseResolve = val => Promise.resolve(val)
4246
exports.PromiseRace = val => Promise.race(val)
47+
exports.Proxy = Proxy
4348
exports.RegExpPrototypeSymbolSplit = (reg, str) => reg[Symbol.split](str)
4449
exports.SafeArrayIterator = class ArrayIterator {constructor (array) { this.array = array }[Symbol.iterator] () { return this.array.values() }}
4550
exports.SafeMap = Map
@@ -60,6 +65,8 @@ exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit)
6065
exports.Symbol = Symbol
6166
exports.SymbolFor = repr => Symbol.for(repr)
6267
exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)
68+
exports.ReflectConstruct = (target, args, newTarget) => Reflect.construct(target, args, newTarget)
69+
exports.ReflectGet = (target, property, receiver) => Reflect.get(target, property, receiver)
6370
exports.RegExpPrototypeExec = (reg, str) => reg.exec(str)
6471
exports.RegExpPrototypeSymbolReplace = (regexp, str, replacement) =>
6572
regexp[Symbol.replace](str, replacement)

0 commit comments

Comments
 (0)