Skip to content

Commit be90007

Browse files
committed
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>
1 parent bbd566d commit be90007

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
@@ -4360,6 +4360,46 @@ test('test', (t) => {
43604360
});
43614361
```
43624362

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

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

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

44044490
<!-- YAML
@@ -5037,6 +5123,7 @@ test.describe('my suite', (suite) => {
50375123
[`SuiteContext`]: #class-suitecontext
50385124
[`TestContext`]: #class-testcontext
50395125
[`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
5126+
[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
50405127
[`assert.throws`]: assert.md#assertthrowsfn-error-message
50415128
[`context.diagnostic`]: #contextdiagnosticmessage
50425129
[`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)