From 226d912d2c93b2e8fc4f8cd7ac49954d61129b89 Mon Sep 17 00:00:00 2001 From: filmaj Date: Sun, 4 May 2025 14:20:32 -0400 Subject: [PATCH] doc: note required ceremony when mocking/testing callback-based APIs When mocking synchronous callback-based APIs, the `MockFunctionContext` tracking mock calls won't be register until after the mock function finishes execution. Tests exercising callback-based code will likely not be written in a way to ensure this is the case. As such, call this out in the documentation. Fixes: https://github.com/nodejs/node/issues/58161 --- doc/api/test.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index d47e6208325499..cd1f43f6a2916b 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -652,6 +652,86 @@ test('spies on an object method', (t) => { }); ``` +### Mocking callback-based APIs + +When mocking callback-based APIs (continuation-passing style), note that call +tracking is updated _after_ the mocked function completes execution. This can +lead to unexpected behavior when inspecting the mock's [`MockFunctionContext`][] +object when asserting on how it was invoked. Using [`process.nextTick()`][] or +[`util.promisify()`][] in your tests can help avoid this problem: + +```mjs +import fs from 'node:fs'; +import { test } from 'node:test'; +import util from 'node:util'; + +test('callback-style API mocking behavior', (t, done) => { + // Mock the fs.writeFile method + t.mock.method(fs, 'writeFile', (path, data, cb) => { + // Invoke callback synchronously + cb(null, 'success'); + }); + + fs.writeFile('test.txt', 'hello', (err, result) => { + // This will show 0 because call tracking is updated after function completion + console.log('Immediate call count:', fs.writeFile.mock.callCount()); + + // Use process.nextTick to check after the event loop tick + process.nextTick(() => { + // This will correctly show 1 + console.log('Call count after nextTick:', fs.writeFile.mock.callCount()); + + // Another approach is to use util.promisify + const writeFilePromise = util.promisify(fs.writeFile); + + // With promises, the call count will be correctly updated + // after the promise is resolved + writeFilePromise('test.txt', 'world') + .then(() => { + console.log('Call count with promises:', fs.writeFile.mock.callCount()); + done(); + }); + }); + }); +}); +``` + +```cjs +const fs = require('node:fs') +const { test } = require('node:test') +const util = require('node:util') + +test('callback-style API mocking behavior', (t, done) => { + // Mock the fs.writeFile method + t.mock.method(fs, 'writeFile', (path, data, cb) => { + // Invoke callback synchronously + cb(null, 'success'); + }); + + fs.writeFile('test.txt', 'hello', (err, result) => { + // This will show 0 because call tracking is updated after function completion + console.log('Immediate call count:', fs.writeFile.mock.callCount()); + + // Use process.nextTick to check after the event loop tick + process.nextTick(() => { + // This will correctly show 1 + console.log('Call count after nextTick:', fs.writeFile.mock.callCount()); + + // Another approach is to use util.promisify + const writeFilePromise = util.promisify(fs.writeFile); + + // With promises, the call count will be correctly updated + // after the promise is resolved + writeFilePromise('test.txt', 'world') + .then(() => { + console.log('Call count with promises:', fs.writeFile.mock.callCount()); + done(); + }); + }); + }); +}); +``` + ### Timers Mocking timers is a technique commonly used in software testing to simulate and @@ -1886,6 +1966,9 @@ mock. Each entry in the array is an object with the following properties. `undefined`. * `this` {any} The mocked function's `this` value. +> When mocking and testing callback-based APIs, please read the +> [Mocking callback-based APIs][mocking callbacks] section. + ### `ctx.callCount()`