- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
Sinon 的stub.callsArgWith(index, ...args)是 stub 行为配置家族中最常用的成员之一:它让 stub 在被调用时,把调用实参列表中位于index位置的参数当作回调函数立即执行,并把预先声明的参数原样传入该回调。本文以 stub.callsArgWith 官方文档 为核心,结合 Sinon 源码与测试用例,讲解其用法、错误处理、底层实现原理,以及与callsArg系列其他方法的选型关系,帮助你写出可复现、可维护的基于回调的测试桩。
功能概述与典型应用场景
stub.callsArgWith(index)的行为是:当 stub 被调用时,将其第index个实参当作回调函数进行调用,并将callsArgWith声明时传入的后续参数(...args)作为该回调的实参。
它最常见的应用场景是模拟 Node.js 风格的 error-first 回调接口,例如:
// 被测代码:调用 API,并在回调中处理结果 function loadUser(api, id, callback) { api.getUser(id, callback); }在测试中,我们并不想真正发起网络请求,而是希望api.getUser被调用时立即以固定参数回调,从而驱动被测逻辑继续执行:
const sinon = require("sinon"); const api = { getUser: sinon.stub() }; api.getUser.callsArgWith(1, null, { id: 1, name: "Alice" }); // 第 1 个参数是回调 loadUser(api, 42, (err, user) => { console.log(user.name); // "Alice" });这样一来,测试不需要等待真实异步 I/O,回调同步执行、结果确定,非常适合单元测试中验证回调驱动的代码路径。
方法签名与参数说明
stub.callsArgWith(index, ...args)| 参数 | 类型 | 说明 |
|---|---|---|
index | number | 回调在 stub 调用实参列表中的位置(从 0 开始计数) |
...args | 任意 | 调用回调时传入的实参列表,原样透传给回调 |
两个关键约定:
index必须是一个数字。若省略或传入非数字(如{}),会立即抛出TypeError(详见下文“错误处理”)。...args可以省略。此时回调会被调用但不传入任何实参(相当于callback()),这一点与 stub.callsArg 的行为一致——区别只在于callsArg不接收、也不传递任何参数给回调。
方法链式返回 stub 本身,因此可以继续级联其他行为配置(如.onCall(...)、.returns(...)等)。
基本用法示例
以下示例取自文档配套测试 docs/tests/docs/stubs/api/calls-arg-with.test.js:
const sinon = require("sinon"); // 第 0 个参数是回调,回调被调用时传入三个水果名 const stub = sinon.stub().callsArgWith(0, "apple", "banana", "cherry"); const callback = sinon.fake(); stub(callback); callback.calledOnce; // true callback.calledWith("apple", "banana", "cherry"); // true对应的核心源码测试位于 test/src/stub-test.js,覆盖了更多细节:
// 回调位于非 0 索引位置 const stub = createStub().callsArgWith(1, object); const callback = createStub(); stub(1, callback); assert(callback.calledWith(object)); // 不传任何回调实参 const stub2 = createStub().callsArgWith(1); stub2(1, callback); assert(callback.calledWith()); // 传多个实参 const stub3 = createStub().callsArgWith(1, object, array); stub3(1, callback); assert(callback.calledWith(object, array));从上面可以看出,回调参数的位置与回调实参的数量彼此独立:index只负责“从哪里取回调”,...args只负责“回调收到什么”。
错误处理
文档明确说明:当指定索引位置的参数不可用或不是函数时,会抛出Error。结合源码,callsArgWith涉及两类共三种错误:
1. 配置期错误:index不是数字
在调用stub.callsArgWith()声明行为时就会立即校验。源码位于 src/sinon/default-behaviors.js:
callsArgWith: function callsArgWith(fake, index) { if (typeof index !== "number") { throw new TypeError("argument index is not number"); } fake.callArgAt = index; fake.callbackArguments = slice(arguments, 2); fake.callbackContext = undefined; fake.callArgProp = undefined; fake.callbackAsync = false; fake.callsThrough = false; },测试验证(test/src/stub-test.js):
stub.callsArgWith(); // TypeError stub.callsArgWith({}); // TypeError2. 调用期错误:实参数量不足
stub 被调用时,如果实参总数小于等于index(即取不到第index个参数),会抛出TypeError。该校验由 src/sinon/behavior.js 中的ensureArgs完成:
function ensureArgs(name, behavior, args) { const property = name.replace(/sArg/, "ArgAt"); // callsArg => callArgAt const index = behavior[property]; if (index >= args.length) { throw new TypeError( `${name} failed: ${index + 1} arguments required but only ${args.length} present`, ); } }3. 调用期错误:该位置参数不是函数
即使实参数量足够,若第index个实参不是函数,也会抛出TypeError。错误消息格式为argument at index ${index} is not a function: ${func}(src/sinon/behavior.js)。文档配套测试给出了直接证据(docs/tests/docs/stubs/api/calls-arg-with.test.js):
const stub = sinon.stub().callsArgWith(0, "apple", "banana", "cherry"); // 实参是 undefined,不是函数 → 抛错 t.throws( () => stub(undefined), /argument at index 0 is not a function/, "throws when argument is not a function" );底层实现原理:调用链剖析
callsArgWith的完整调用链分为“配置”与“调用”两个阶段。
配置阶段:写入 stub 行为
当执行stub.callsArgWith(1, "a", "b")时,src/sinon/behavior.js 中的createBehavior会将callsArgWith注册到 stub 的默认行为对象上,并写入以下内部状态(src/sinon/default-behaviors.js):
callArgAt = index:记录回调在实参列表中的位置;callbackArguments = [...args]:记录要透传给回调的实参(slice(arguments, 2)截取除fake与index之外的所有参数);callbackContext = undefined:回调的this指向(callsArgWith不指定上下文,相关能力由callsArgOnWith提供);callbackAsync = false:同步调用(异步版本见下文);callsThrough = false:覆盖此前可能设置的callThrough行为。
调用阶段:提取并执行回调
stub 被调用时,src/sinon/behavior.js 的invoke会最先执行callCallback(其位置刻意放在所有其他行为之前):
function callCallback(behavior, args) { if (typeof behavior.callArgAt === "number") { ensureArgs("callsArg", behavior, args); // 1. 校验实参数量 const func = getCallback(behavior, args); // 2. 按索引取出回调 if (typeof func !== "function") { // 3. 校验是函数 throw new TypeError(getCallbackError(behavior, func, args)); } if (behavior.callbackAsync) { nextTick(function () { func.apply(behavior.callbackContext, behavior.callbackArguments); }); } else { return func.apply(behavior.callbackContext, behavior.callbackArguments); } } return undefined; }其中getCallback(src/sinon/behavior.js)在callArgAt >= 0时直接返回args[callArgAt],即按索引精确取参——这正是callsArg/callsArgWith家族与yields家族(自动寻找“最左/最右回调”)的本质区别。
返回值语义
invoke中callCallback的返回值会被保留,并在没有其他行为(如returns、throws)抢占时作为 stub 的返回值返回(src/sinon/behavior.js)。因此:stub.callsArgWith(...)的返回值就是回调函数的返回值。测试也验证了这一点(test/src/stub-test.js):
const stub = sinon.stub().callsArgWith(0, "test"); const callback = sinon.stub().returns("return value"); stub(callback); // === "return value"组合用法:与 onCall、callThrough 配合
callsArgWith返回 stub 本身,可与其他行为链式组合,实现“不同调用次数执行不同行为”:
const stub = sinon.stub(); stub .onFirstCall().callsArgWith(0, "first") // 第 1 次调用:回调收到 "first" .onSecondCall().callsArgWith(1, "a", "b") // 第 2 次调用:取第 1 个参数作回调 .onThirdCall().callsArgOn(2, context); // 第 3 次调用:指定回调 this const spy = sinon.spy(); stub(spy); // spy 收到 "first" stub(1, spy); // spy 收到 "a", "b"该组合场景同样有源码测试覆盖(test/src/stub-test.js)。此外,callsArgWith会覆盖先前设置的callThrough(内部将callsThrough置为false),测试见 test/src/stub-test.js:
const stub = sinon.stub(obj, "fn").callThrough().callsArgWith(0, "test"); const callback = sinon.stub().returns("return value"); stub(callback); // 不再穿透调用 obj.fn,而是调用 callback异步版本:callsArgWithAsync
如需回调异步触发(在下一次 tick 中执行),可使用 stub.callsArgWithAsync。它并非独立实现,而是由 src/sinon/util/core/export-async-behaviors.js 自动生成:对名称匹配/^(callsArg|yields)/且不含Async的方法,生成同名Async版本,仅将callbackAsync置为true,其余逻辑完全复用同步版本。同步版本内部通过nextTick调度,保证回调不会在当前调用栈内同步执行:
const stub = sinon.stub().callsArgWithAsync(0, "result"); let called = false; stub(() => { called = true; }); called; // false —— 回调尚未执行,已调度到下一个 tickcallsArgWith 与 callsArg 系列方法选型对照
callsArgWith属于以“指定索引取回调”为核心的callsArg方法族,下表梳理了各成员与本文方法的差异:
| 方法 | 回调位置 | 回调实参 | 回调this | 触发时机 |
|---|---|---|---|---|
stub.callsArg | 指定index | 无 | undefined | 同步 |
stub.callsArgWith | 指定index | 自定...args | undefined | 同步 |
stub.callsArgOn | 指定index | 无 | 指定context | 同步 |
stub.callsArgOnWith | 指定index | 自定...args | 指定context | 同步 |
stub.callsArgAsync | 指定index | 无 | undefined | 下一 tick |
stub.callsArgWithAsync | 指定index | 自定...args | undefined | 下一 tick |
stub.callsArgOnAsync | 指定index | 无 | 指定context | 下一 tick |
stub.callsArgOnWithAsync | 指定index | 自定...args | 指定context | 下一 tick |
选型建议:当回调需要接收固定参数(如 error-first 回调的(err, data))时,callsArgWith是首选;若还需控制回调内部的this,则升级为callsArgOnWith;若回调不需要任何参数,直接用更简洁的callsArg即可。若回调位置不固定、希望自动寻找参数列表中最左/最右的函数,则应改用yields/yieldsRight家族(参见 stubs 概念文档 与 stub.yields)。
小结
stub.callsArgWith(index, ...args)是 Sinon 中“以参数位置定位回调、以固定实参驱动回调”的标准工具,其核心价值在于:在不执行真实依赖的前提下,精确控制回调的触发时机与入参,从而稳定驱动被测代码的回调分支。使用时牢记三点:index必须是数字(配置期抛TypeError)、实参数量必须超过index(调用期抛TypeError)、该位置实参必须是函数(调用期抛TypeError);它同步触发回调、返回值为回调的返回值,可放心与onCall链式组合构建多阶段行为。完整的 Stub API 列表可查阅 Stub API 索引,更深入的行为机制可阅读 src/sinon/default-behaviors.js 与 src/sinon/behavior.js。
- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
相关推荐
SpacetimeDB Unreal SDK Types 目录深度解析:从 ClientAPI 线协议镜像到 UE 值类型桥接
SpacetimeDB Unreal SDK Types 目录深度解析:从 ClientAPI 线协议镜像到 UE 值类型桥接 本篇技术指南以 sdks/unr
测试开发工具Sinon stub.callArg 深入解析:按索引精准触发 stub 回调函数
Sinon stub.callArg 深入解析:按索引精准触发 stub 回调函数 stub.callArg index 是 Sinon 中用于“主动触发”st
测试开发工具Sinon 深入:stub.callsArgOnAsync —— 异步触发指定参数回调并绑定 this 上下文
Sinon 深入:stub.callsArgOnAsync —— 异步触发指定参数回调并绑定 this 上下文 stub.callsArgOnAsync ind
测试开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考