test_runner: add mock call assertions to t.assert

Add t.assert.called(), t.assert.callCount(), t.assert.nthCalledWith()
and t.assert.lastCalledWith() for asserting on mock function calls.

Assisted-by: Claude
Signed-off-by: Guilherme Araújo <arauujogui@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66476
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Guilherme Araújo authored and GitHub committed 2026-10-06 05:55:47 +00:00
1 parent 9a85290d86
commit fcfb7ecc0b
4 files changed
+276 -1

No files matched your search

+87
View File
@@ -4359,6 +4359,46 @@ test('test', (t) => {
});
```
#### `context.assert.callCount(fn, times[, message])`
<!-- YAML
added: REPLACEME
-->
* `fn` {Function} A mock function created by the test runner's mocking API.
* `times` {integer} The expected number of calls.
* `message` {string} Optional error message.
Asserts that the mock function `fn` has been called exactly `times` times.
```js
test('mock was called twice', (t) => {
const fn = t.mock.fn();
fn();
fn();
t.assert.callCount(fn, 2);
});
```
#### `context.assert.called(fn[, message])`
<!-- YAML
added: REPLACEME
-->
* `fn` {Function} A mock function created by the test runner's mocking API.
* `message` {string} Optional error message.
Asserts that the mock function `fn` has been called at least once.
```js
test('mock was called', (t) => {
const fn = t.mock.fn();
fn();
t.assert.called(fn);
});
```
#### `context.assert.fileSnapshot(value, path[, options])`
<!-- YAML
@@ -4398,6 +4438,52 @@ This function differs from `context.assert.snapshot()` in the following ways:
These differences allow snapshot files to better support features such as syntax
highlighting.
#### `context.assert.lastCalledWith(fn[, ...args])`
<!-- YAML
added: REPLACEME
-->
* `fn` {Function} A mock function created by the test runner's mocking API.
* `...args` {any} The expected arguments.
Asserts that the most recent call to the mock function `fn` received arguments
deeply and strictly equal to `args`, using the same comparison as
[`assert.deepStrictEqual()`][].
```js
test('mock was last called with arguments', (t) => {
const fn = t.mock.fn();
fn(1);
fn(2);
t.assert.lastCalledWith(fn, 2);
});
```
#### `context.assert.nthCalledWith(fn, n[, ...args])`
<!-- YAML
added: REPLACEME
-->
* `fn` {Function} A mock function created by the test runner's mocking API.
* `n` {integer} The 1-based index of the call to check.
* `...args` {any} The expected arguments.
Asserts that the `n`th call to the mock function `fn` received arguments
deeply and strictly equal to `args`, using the same comparison as
[`assert.deepStrictEqual()`][]. `n` starts at `1`, so
`t.assert.nthCalledWith(fn, 1)` checks `fn.mock.calls[0]`.
```js
test('mock was called with arguments on the second call', (t) => {
const fn = t.mock.fn();
fn(1);
fn(2);
t.assert.nthCalledWith(fn, 2, 2);
});
```
#### `context.assert.snapshot(value[, options])`
<!-- YAML
@@ -5036,6 +5122,7 @@ test.describe('my suite', (suite) => {
[`SuiteContext`]: #class-suitecontext
[`TestContext`]: #class-testcontext
[`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
[`assert.throws`]: assert.md#assertthrowsfn-error-message
[`context.diagnostic`]: #contextdiagnosticmessage
[`context.log`]: #contextlogmessage-data
+105
View File
@@ -2,11 +2,19 @@
const {
SafeMap,
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
},
} = require('internal/errors');
const {
validateFunction,
validateInteger,
validateString,
} = require('internal/validators');
const { isDeepStrictEqual } = require('internal/util/comparisons');
const assert = require('assert');
const { AssertionError } = assert;
const methodsToCopy = [
'deepEqual',
'deepStrictEqual',
@@ -28,6 +36,98 @@ const methodsToCopy = [
];
let assertMap;
function getMockContext(fn) {
const { MockFunctionContext } = require('internal/test_runner/mock/mock');
const ctx = fn?.mock;
if (!(ctx instanceof MockFunctionContext)) {
throw new ERR_INVALID_ARG_TYPE('fn', 'mock function', fn);
}
return ctx;
}
function called(fn, message) {
const count = getMockContext(fn).callCount();
if (count === 0) {
throw new AssertionError({
__proto__: null,
message: message ?? 'Expected mock function to have been called',
actual: count,
operator: 'called',
stackStartFn: called,
});
}
}
function callCount(fn, times, message) {
validateInteger(times, 'times', 0);
const count = getMockContext(fn).callCount();
if (count !== times) {
throw new AssertionError({
__proto__: null,
message: message ??
`Expected mock function to have been called ${times} times, but it was called ${count} times`,
actual: count,
expected: times,
operator: 'callCount',
stackStartFn: callCount,
});
}
}
function assertCalledWithAt(fn, n, args, stackStartFn) {
const calls = getMockContext(fn).calls;
if (n > calls.length) {
throw new AssertionError({
__proto__: null,
message: `Expected mock function to have been called at least ${n} times, ` +
`but it was called ${calls.length} times`,
actual: calls.length,
expected: n,
operator: stackStartFn.name,
stackStartFn,
});
}
const actual = calls[n - 1].arguments;
if (!isDeepStrictEqual(actual, args)) {
throw new AssertionError({
__proto__: null,
message: `Expected call ${n} of mock function to have been called with:`,
actual,
expected: args,
operator: 'deepStrictEqual',
stackStartFn,
});
}
}
function nthCalledWith(fn, n, ...args) {
validateInteger(n, 'n', 1);
assertCalledWithAt(fn, n, args, nthCalledWith);
}
function lastCalledWith(fn, ...args) {
const count = getMockContext(fn).callCount();
if (count === 0) {
throw new AssertionError({
__proto__: null,
message: 'Expected mock function to have been called',
actual: count,
operator: 'lastCalledWith',
stackStartFn: lastCalledWith,
});
}
assertCalledWithAt(fn, count, args, lastCalledWith);
}
function getAssertionMap() {
if (assertMap === undefined) {
assertMap = new SafeMap();
@@ -35,6 +135,11 @@ function getAssertionMap() {
for (let i = 0; i < methodsToCopy.length; i++) {
assertMap.set(methodsToCopy[i], assert[methodsToCopy[i]]);
}
assertMap.set('called', called);
assertMap.set('callCount', callCount);
assertMap.set('lastCalledWith', lastCalledWith);
assertMap.set('nthCalledWith', nthCalledWith);
}
return assertMap;
+1
View File
@@ -1118,5 +1118,6 @@ if (!enableModuleMocking) {
module.exports = {
ensureNodeScheme,
MockFunctionContext,
MockTracker,
};
+83 -1
View File
@@ -11,10 +11,92 @@ test('expected methods are on t.assert', (t) => {
'options',
];
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();
const expectedKeys = [
'snapshot',
'fileSnapshot',
'called',
'callCount',
'lastCalledWith',
'nthCalledWith',
].concat(assertKeys).sort();
assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
});
test('t.assert.ok correctly parses the stacktrace', (t) => {
t.assert.throws(() => t.assert.ok(1 === 2), /t\.assert\.ok\(1 === 2\)/);
});
test('t.assert.called', (t) => {
const fn = t.mock.fn();
t.assert.throws(() => t.assert.called(fn), {
code: 'ERR_ASSERTION',
message: 'Expected mock function to have been called',
});
fn();
t.assert.called(fn);
t.assert.throws(() => t.assert.called(() => {}), {
code: 'ERR_INVALID_ARG_TYPE',
});
});
test('t.assert.callCount', (t) => {
const fn = t.mock.fn();
t.assert.callCount(fn, 0);
fn();
fn();
t.assert.callCount(fn, 2);
t.assert.throws(() => t.assert.callCount(fn, 1), {
code: 'ERR_ASSERTION',
message: 'Expected mock function to have been called 1 times, but it was called 2 times',
});
t.assert.throws(() => t.assert.callCount(fn, -1), {
code: 'ERR_OUT_OF_RANGE',
});
});
test('t.assert.nthCalledWith', (t) => {
const fn = t.mock.fn();
fn(1);
fn({ a: 2 });
t.assert.nthCalledWith(fn, 1, 1);
t.assert.nthCalledWith(fn, 2, { a: 2 });
t.assert.throws(() => t.assert.nthCalledWith(fn, 2, { a: 3 }), {
code: 'ERR_ASSERTION',
message: /^Expected call 2 of mock function to have been called with:/,
actual: [{ a: 2 }],
expected: [{ a: 3 }],
});
t.assert.throws(() => t.assert.nthCalledWith(fn, 3, 1), {
code: 'ERR_ASSERTION',
message: 'Expected mock function to have been called at least 3 times, but it was called 2 times',
});
t.assert.throws(() => t.assert.nthCalledWith(fn, 0, 1), {
code: 'ERR_OUT_OF_RANGE',
});
});
test('t.assert.lastCalledWith', (t) => {
const fn = t.mock.fn();
t.assert.throws(() => t.assert.lastCalledWith(fn), {
code: 'ERR_ASSERTION',
message: 'Expected mock function to have been called',
});
fn(1);
fn(2);
t.assert.lastCalledWith(fn, 2);
t.assert.throws(() => t.assert.lastCalledWith(fn, 1), {
code: 'ERR_ASSERTION',
actual: [2],
expected: [1],
});
});
test('mock matchers count towards t.plan()', (t) => {
t.plan(4);
const fn = t.mock.fn();
fn(1);
t.assert.called(fn);
t.assert.callCount(fn, 1);
t.assert.nthCalledWith(fn, 1, 1);
t.assert.lastCalledWith(fn, 1);
});