nodejs/node · #66476

test_runner: add mock call assertions to t.assert

araujogui · merged Oct 6, 20264 files · 276 + / 1 −
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-data
lib/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);+});