Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -4360,6 +4360,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
Expand Down Expand Up @@ -4399,6 +4439,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
Expand Down Expand Up @@ -5037,6 +5123,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
Expand Down
105 changes: 105 additions & 0 deletions lib/internal/test_runner/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -28,13 +36,110 @@ 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();

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;
Expand Down
1 change: 1 addition & 0 deletions lib/internal/test_runner/mock/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -1118,5 +1118,6 @@ if (!enableModuleMocking) {

module.exports = {
ensureNodeScheme,
MockFunctionContext,
MockTracker,
};
84 changes: 83 additions & 1 deletion test/parallel/test-runner-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading