nodejs/node · #66476
test_runner: add mock call assertions to t.assert
doc/api/test.md87 + / 0 −
@@ -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@@ -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@@ -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-datalib/internal/test_runner/assert.js105 + / 0 −
@@ -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,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;lib/internal/test_runner/mock/mock.js1 + / 0 −
@@ -1118,5 +1118,6 @@ if (!enableModuleMocking) { module.exports = { ensureNodeScheme,+ MockFunctionContext, MockTracker, };test/parallel/test-runner-assert.js83 + / 1 −
@@ -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);+});