Skip to content

Commit fcfb7ec

Browse files
authored
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: #66476 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 9a85290 commit fcfb7ec

4 files changed

Lines changed: 276 additions & 1 deletion

File tree

‎doc/api/test.md‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4359,6 +4359,46 @@ test('test', (t) => {
43594359
});
43604360
```
43614361

4362+
#### `context.assert.callCount(fn, times[, message])`
4363+
4364+
<!-- YAML
4365+
added: REPLACEME
4366+
-->
4367+
4368+
* `fn` {Function} A mock function created by the test runner's mocking API.
4369+
* `times` {integer} The expected number of calls.
4370+
* `message` {string} Optional error message.
4371+
4372+
Asserts that the mock function `fn` has been called exactly `times` times.
4373+
4374+
```js
4375+
test('mock was called twice', (t) => {
4376+
const fn = t.mock.fn();
4377+
fn();
4378+
fn();
4379+
t.assert.callCount(fn, 2);
4380+
});
4381+
```
4382+
4383+
#### `context.assert.called(fn[, message])`
4384+
4385+
<!-- YAML
4386+
added: REPLACEME
4387+
-->
4388+
4389+
* `fn` {Function} A mock function created by the test runner's mocking API.
4390+
* `message` {string} Optional error message.
4391+
4392+
Asserts that the mock function `fn` has been called at least once.
4393+
4394+
```js
4395+
test('mock was called', (t) => {
4396+
const fn = t.mock.fn();
4397+
fn();
4398+
t.assert.called(fn);
4399+
});
4400+
```
4401+
43624402
#### `context.assert.fileSnapshot(value, path[, options])`
43634403

43644404
<!-- YAML
@@ -4398,6 +4438,52 @@ This function differs from `context.assert.snapshot()` in the following ways:
43984438
These differences allow snapshot files to better support features such as syntax
43994439
highlighting.
44004440

4441+
#### `context.assert.lastCalledWith(fn[, ...args])`
4442+
4443+
<!-- YAML
4444+
added: REPLACEME
4445+
-->
4446+
4447+
* `fn` {Function} A mock function created by the test runner's mocking API.
4448+
* `...args` {any} The expected arguments.
4449+
4450+
Asserts that the most recent call to the mock function `fn` received arguments
4451+
deeply and strictly equal to `args`, using the same comparison as
4452+
[`assert.deepStrictEqual()`][].
4453+
4454+
```js
4455+
test('mock was last called with arguments', (t) => {
4456+
const fn = t.mock.fn();
4457+
fn(1);
4458+
fn(2);
4459+
t.assert.lastCalledWith(fn, 2);
4460+
});
4461+
```
4462+
4463+
#### `context.assert.nthCalledWith(fn, n[, ...args])`
4464+
4465+
<!-- YAML
4466+
added: REPLACEME
4467+
-->
4468+
4469+
* `fn` {Function} A mock function created by the test runner's mocking API.
4470+
* `n` {integer} The 1-based index of the call to check.
4471+
* `...args` {any} The expected arguments.
4472+
4473+
Asserts that the `n`th call to the mock function `fn` received arguments
4474+
deeply and strictly equal to `args`, using the same comparison as
4475+
[`assert.deepStrictEqual()`][]. `n` starts at `1`, so
4476+
`t.assert.nthCalledWith(fn, 1)` checks `fn.mock.calls[0]`.
4477+
4478+
```js
4479+
test('mock was called with arguments on the second call', (t) => {
4480+
const fn = t.mock.fn();
4481+
fn(1);
4482+
fn(2);
4483+
t.assert.nthCalledWith(fn, 2, 2);
4484+
});
4485+
```
4486+
44014487
#### `context.assert.snapshot(value[, options])`
44024488

44034489
<!-- YAML
@@ -5036,6 +5122,7 @@ test.describe('my suite', (suite) => {
50365122
[`SuiteContext`]: #class-suitecontext
50375123
[`TestContext`]: #class-testcontext
50385124
[`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
5125+
[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
50395126
[`assert.throws`]: assert.md#assertthrowsfn-error-message
50405127
[`context.diagnostic`]: #contextdiagnosticmessage
50415128
[`context.log`]: #contextlogmessage-data

‎lib/internal/test_runner/assert.js‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
const {
33
SafeMap,
44
} = primordials;
5+
const {
6+
codes: {
7+
ERR_INVALID_ARG_TYPE,
8+
},
9+
} = require('internal/errors');
510
const {
611
validateFunction,
12+
validateInteger,
713
validateString,
814
} = require('internal/validators');
15+
const { isDeepStrictEqual } = require('internal/util/comparisons');
916
const assert = require('assert');
17+
const { AssertionError } = assert;
1018
const methodsToCopy = [
1119
'deepEqual',
1220
'deepStrictEqual',
@@ -28,13 +36,110 @@ const methodsToCopy = [
2836
];
2937
let assertMap;
3038

39+
function getMockContext(fn) {
40+
const { MockFunctionContext } = require('internal/test_runner/mock/mock');
41+
const ctx = fn?.mock;
42+
43+
if (!(ctx instanceof MockFunctionContext)) {
44+
throw new ERR_INVALID_ARG_TYPE('fn', 'mock function', fn);
45+
}
46+
47+
return ctx;
48+
}
49+
50+
function called(fn, message) {
51+
const count = getMockContext(fn).callCount();
52+
53+
if (count === 0) {
54+
throw new AssertionError({
55+
__proto__: null,
56+
message: message ?? 'Expected mock function to have been called',
57+
actual: count,
58+
operator: 'called',
59+
stackStartFn: called,
60+
});
61+
}
62+
}
63+
64+
function callCount(fn, times, message) {
65+
validateInteger(times, 'times', 0);
66+
const count = getMockContext(fn).callCount();
67+
68+
if (count !== times) {
69+
throw new AssertionError({
70+
__proto__: null,
71+
message: message ??
72+
`Expected mock function to have been called ${times} times, but it was called ${count} times`,
73+
actual: count,
74+
expected: times,
75+
operator: 'callCount',
76+
stackStartFn: callCount,
77+
});
78+
}
79+
}
80+
81+
function assertCalledWithAt(fn, n, args, stackStartFn) {
82+
const calls = getMockContext(fn).calls;
83+
84+
if (n > calls.length) {
85+
throw new AssertionError({
86+
__proto__: null,
87+
message: `Expected mock function to have been called at least ${n} times, ` +
88+
`but it was called ${calls.length} times`,
89+
actual: calls.length,
90+
expected: n,
91+
operator: stackStartFn.name,
92+
stackStartFn,
93+
});
94+
}
95+
96+
const actual = calls[n - 1].arguments;
97+
98+
if (!isDeepStrictEqual(actual, args)) {
99+
throw new AssertionError({
100+
__proto__: null,
101+
message: `Expected call ${n} of mock function to have been called with:`,
102+
actual,
103+
expected: args,
104+
operator: 'deepStrictEqual',
105+
stackStartFn,
106+
});
107+
}
108+
}
109+
110+
function nthCalledWith(fn, n, ...args) {
111+
validateInteger(n, 'n', 1);
112+
assertCalledWithAt(fn, n, args, nthCalledWith);
113+
}
114+
115+
function lastCalledWith(fn, ...args) {
116+
const count = getMockContext(fn).callCount();
117+
118+
if (count === 0) {
119+
throw new AssertionError({
120+
__proto__: null,
121+
message: 'Expected mock function to have been called',
122+
actual: count,
123+
operator: 'lastCalledWith',
124+
stackStartFn: lastCalledWith,
125+
});
126+
}
127+
128+
assertCalledWithAt(fn, count, args, lastCalledWith);
129+
}
130+
31131
function getAssertionMap() {
32132
if (assertMap === undefined) {
33133
assertMap = new SafeMap();
34134

35135
for (let i = 0; i < methodsToCopy.length; i++) {
36136
assertMap.set(methodsToCopy[i], assert[methodsToCopy[i]]);
37137
}
138+
139+
assertMap.set('called', called);
140+
assertMap.set('callCount', callCount);
141+
assertMap.set('lastCalledWith', lastCalledWith);
142+
assertMap.set('nthCalledWith', nthCalledWith);
38143
}
39144

40145
return assertMap;

‎lib/internal/test_runner/mock/mock.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,5 +1118,6 @@ if (!enableModuleMocking) {
11181118

11191119
module.exports = {
11201120
ensureNodeScheme,
1121+
MockFunctionContext,
11211122
MockTracker,
11221123
};

‎test/parallel/test-runner-assert.js‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,92 @@ test('expected methods are on t.assert', (t) => {
1111
'options',
1212
];
1313
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
14-
const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();
14+
const expectedKeys = [
15+
'snapshot',
16+
'fileSnapshot',
17+
'called',
18+
'callCount',
19+
'lastCalledWith',
20+
'nthCalledWith',
21+
].concat(assertKeys).sort();
1522
assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
1623
});
1724

1825
test('t.assert.ok correctly parses the stacktrace', (t) => {
1926
t.assert.throws(() => t.assert.ok(1 === 2), /t\.assert\.ok\(1 === 2\)/);
2027
});
28+
29+
test('t.assert.called', (t) => {
30+
const fn = t.mock.fn();
31+
t.assert.throws(() => t.assert.called(fn), {
32+
code: 'ERR_ASSERTION',
33+
message: 'Expected mock function to have been called',
34+
});
35+
fn();
36+
t.assert.called(fn);
37+
t.assert.throws(() => t.assert.called(() => {}), {
38+
code: 'ERR_INVALID_ARG_TYPE',
39+
});
40+
});
41+
42+
test('t.assert.callCount', (t) => {
43+
const fn = t.mock.fn();
44+
t.assert.callCount(fn, 0);
45+
fn();
46+
fn();
47+
t.assert.callCount(fn, 2);
48+
t.assert.throws(() => t.assert.callCount(fn, 1), {
49+
code: 'ERR_ASSERTION',
50+
message: 'Expected mock function to have been called 1 times, but it was called 2 times',
51+
});
52+
t.assert.throws(() => t.assert.callCount(fn, -1), {
53+
code: 'ERR_OUT_OF_RANGE',
54+
});
55+
});
56+
57+
test('t.assert.nthCalledWith', (t) => {
58+
const fn = t.mock.fn();
59+
fn(1);
60+
fn({ a: 2 });
61+
t.assert.nthCalledWith(fn, 1, 1);
62+
t.assert.nthCalledWith(fn, 2, { a: 2 });
63+
t.assert.throws(() => t.assert.nthCalledWith(fn, 2, { a: 3 }), {
64+
code: 'ERR_ASSERTION',
65+
message: /^Expected call 2 of mock function to have been called with:/,
66+
actual: [{ a: 2 }],
67+
expected: [{ a: 3 }],
68+
});
69+
t.assert.throws(() => t.assert.nthCalledWith(fn, 3, 1), {
70+
code: 'ERR_ASSERTION',
71+
message: 'Expected mock function to have been called at least 3 times, but it was called 2 times',
72+
});
73+
t.assert.throws(() => t.assert.nthCalledWith(fn, 0, 1), {
74+
code: 'ERR_OUT_OF_RANGE',
75+
});
76+
});
77+
78+
test('t.assert.lastCalledWith', (t) => {
79+
const fn = t.mock.fn();
80+
t.assert.throws(() => t.assert.lastCalledWith(fn), {
81+
code: 'ERR_ASSERTION',
82+
message: 'Expected mock function to have been called',
83+
});
84+
fn(1);
85+
fn(2);
86+
t.assert.lastCalledWith(fn, 2);
87+
t.assert.throws(() => t.assert.lastCalledWith(fn, 1), {
88+
code: 'ERR_ASSERTION',
89+
actual: [2],
90+
expected: [1],
91+
});
92+
});
93+
94+
test('mock matchers count towards t.plan()', (t) => {
95+
t.plan(4);
96+
const fn = t.mock.fn();
97+
fn(1);
98+
t.assert.called(fn);
99+
t.assert.callCount(fn, 1);
100+
t.assert.nthCalledWith(fn, 1, 1);
101+
t.assert.lastCalledWith(fn, 1);
102+
});

0 commit comments

Comments
 (0)