- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
导读
stub.onCall(index)是 Sinon stub 中专门用于按调用次序(第 N 次调用)定制行为的 API,它让"第一次调用返回 A、第二次调用返回 B、第三次抛异常"这类顺序交互场景变得直接可写。本文以官方文档 docs/concepts/stubs/api/on-call.md 为主体,结合源码实现(src/sinon/stub.js、src/sinon/behavior.js)与仓库测试(test/src/stub-test.js、docs/tests/docs/stubs/api/on-call-1.test.js)进行纵深解读,你将掌握:onCall的索引语义与回退机制、三个便捷别名的用法、与withArgs组合的正确姿势(及一个必须避开的反模式),以及它在重试、限流、分页等真实业务场景中的落地写法。
onCall(index)是什么:按调用次序精准定制
stub.onCall(index)用于定义 stub 在第n次调用时的行为,索引index从0开始计数(即onCall(0)对应第一次调用)。它非常适合测试顺序交互:例如轮询、重试逻辑、逐页拉取数据等场景中,同一函数在不同调用轮次需要返回不同结果。
官方文档给出的定位是:
Defines the behavior of the stub on thenthcall. Useful for testing sequential interactions.
从源码看,其实现非常直接(src/sinon/stub.js):
onCall: function onCall(index) { if (!this.behaviors[index]) { this.behaviors[index] = behavior.create(this); } return this.behaviors[index]; },每次调用onCall(index)时,stub 会按索引把行为对象缓存在内部的behaviors数组中(不存在则新建),并返回该行为对象——这正是它可以继续链式调用returns、throws、callsFake等行为方法的原因。注意:仅调用onCall(index)本身不会产生任何行为副作用,它只是"取到"或"创建"了一个行为容器,真正生效的是随后挂到它上面的行为定义(这一点在 test/src/stub-test.js 中有专门用例验证:does not create undefined behaviour just by calling onCall)。
基础用法:按调用顺序返回不同值
仓库文档配套测试 docs/tests/docs/stubs/api/on-call-1.test.js 给出了最经典的"顺序返回值"示例:
import tap from "tap"; import * as sinon from "sinon"; const callback = sinon.stub(); callback.onCall(0).returns("Apple pie"); callback.onCall(1).returns("Blueberry pie"); callback.returns("Raspberry pie"); // 默认行为(回退值) callback(); // => "Apple pie" 第一次调用 callback(); // => "Blueberry pie" 第二次调用 callback(); // => "Raspberry pie" 第三次调用(回退到默认) callback(); // => "Raspberry pie" 后续所有调用(继续回退到默认)关键点有三:
- 索引从 0 开始:
onCall(0)管第一次调用,onCall(1)管第二次调用,依此类推; - 可以跳跃式指定:无需连续铺满每个索引,只声明关心的那几次即可;
- 回退机制:一旦声明的指定行为用尽(第 3 次及以后),stub 自动回退到默认行为(这里是由
callback.returns("Raspberry pie")定义的默认值)。
回退机制在源码中的实现是 src/sinon/stub.js 的getCurrentBehavior:
function getCurrentBehavior(stubInstance) { const currentBehavior = stubInstance.behaviors[stubInstance.callCount - 1]; return currentBehavior && currentBehavior.isPresent() ? currentBehavior : getDefaultBehavior(stubInstance); }每次调用时,Sinon 先根据callCount从behaviors数组取出对应索引的行为;只有当该行为确实存在且已被定义过(isPresent())时才会使用它,否则一律回退到defaultBehavior(即returns(...)、throws(...)等设置的默认行为,见 src/sinon/stub.js 的getDefaultBehavior)。这正是"指定行为用尽后回退默认"的底层原理。
便捷别名:onFirstCall/onSecondCall/onThirdCall
为了让 stub 定义更易读,Sinon 提供了三个纯语法糖别名,它们与onCall(index)完全等价(见 src/sinon/stub.js):
| 别名 | 等价写法 | 语义 |
|---|---|---|
stub.onFirstCall() | stub.onCall(0) | 第一次调用的行为 |
stub.onSecondCall() | stub.onCall(1) | 第二次调用的行为 |
stub.onThirdCall() | stub.onCall(2) | 第三次调用的行为 |
对应文档分别为 docs/concepts/stubs/api/on-first-call.md、docs/concepts/stubs/api/on-second-call.md、docs/concepts/stubs/api/on-third-call.md。
仓库文档配套测试 docs/tests/docs/stubs/api/on-first-call.test.js 演示了onFirstCall的用法:
const callback = sinon.stub(); callback.onFirstCall().returns("Apple pie"); callback.returns("Raspberry pie"); callback(); // => "Apple pie" 第一次调用 callback(); // => "Raspberry pie" 第二次调用(回退默认) callback(); // => "Raspberry pie" 后续所有调用(回退默认)别名可以混合、可以链式
三个别名不仅彼此可混用,还能与onCall直接链式拼接。仓库核心测试 test/src/stub-test.js 展示了"别名 + 默认 + 别名"的混合序列:
const stub = sinon.stub().returns(3); // 默认:返回 3 stub.onFirstCall().returns(1).onCall(2).returns(2); // 第 1 次返回 1,第 3 次返回 2 stub(); // => 1 (第 1 次,命中 onFirstCall) stub(); // => 3 (第 2 次,无指定行为,回退默认) stub(); // => 2 (第 3 次,命中 onCall(2)) stub(); // => 3 (第 4 次,指定行为用尽,回退默认)链式声明一整段序列
onCall返回行为对象,而行为对象上的方法(如returns)会返回 stub 自身,因此可以把整段顺序行为串成一条链。官方测试同样覆盖了这种写法(test/src/stub-test.js):
const stub = sinon.stub() .onCall(0).returns(1) .onCall(1).returns(2) .onCall(2).returns(3); stub(); // => 1 stub(); // => 2 stub(); // => 3与withArgs组合:按参数 + 按次序双重定制
onCall可以与本 API 中所有行为定义方法组合使用,其中最有价值的是与withArgs的组合——它允许你针对"特定参数的调用"再按调用次序分别定制行为。
官方文档配套测试 docs/tests/docs/stubs/api/on-call-2.test.js 给出了完整示例:
const callback = sinon.stub(); const FORTY_TWO = 42; const UNKNOWN_VALUE = "any unknown value"; callback .withArgs(FORTY_TWO) .onFirstCall() .returns("Apple pie") .onSecondCall() .returns("Blueberry pie"); callback.returns("Raspberry pie"); // 默认行为 callback(UNKNOWN_VALUE); // => "Raspberry pie" 未知参数,走默认 callback(FORTY_TWO); // => "Apple pie" 第 1 次传 42 callback(UNKNOWN_VALUE); // => "Raspberry pie" 未知参数,走默认 callback(FORTY_TWO); // => "Blueberry pie" 第 2 次传 42 callback(FORTY_TWO); // => "Raspberry pie" 第 3 次传 42,指定行为用尽,回退默认这个例子的精妙之处在于**"回退"是分层级的**:为参数42定义的指定行为(第 1、2 次)用尽后,callback(FORTY_TWO)回退到的并非全局默认行为"Raspberry pie"(因为withArgs创建的 fake 没有自己的默认行为),而是先尝试 fake 自身的默认行为,没有则最终落到 stub 的默认行为。官方文档特意提醒读者注意:
Note how the behavior of the stub for argument
FORTY_TWOfalls back to the default behavior once no more calls have been defined.
仓库核心测试 test/src/stub-test.js 还演示了"withArgsfake 自带默认行为 + 指定调用行为"的完整序列:
const stub = sinon.stub().returns(0); // stub 默认:返回 0 stub.withArgs(5) .returns(-1) // fake 默认:返回 -1 .onFirstCall().returns(1) // 第 1 次传 5:返回 1 .onSecondCall().returns(2); // 第 2 次传 5:返回 2 stub(0); // => 0 不匹配参数,走 stub 默认 stub(5); // => 1 第 1 次传 5 stub(0); // => 0 不匹配参数,走 stub 默认 stub(5); // => 2 第 2 次传 5 stub(5); // => -1 第 3 次传 5:指定行为用尽,回退到 fake 自己的默认组合顺序:必须先withArgs再onCall(反之会抛错)
顺序是有讲究的:正确写法是stub.withArgs(...).onCall(...),而stub.onCall(...).withArgs(...)是不被支持的反模式。后者会在运行时直接抛出错误,错误信息明确指出正确用法(见 src/sinon/behavior.js):
Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. Use "stub.withArgs(...).onCall(...)" instead.核心测试 test/src/behavior-extra-test.js 专门锁定这一行为,确保后续版本不会悄悄放行这种写法。
与更多行为方法组合:returnsArg、returnsThis、throws等
onCall的"第 N 次"定制并不局限于返回值,它可以与 stub API 中的全部行为定义方法组合使用。仓库核心测试从多个维度验证了这一点(test/src/stub-test.js):
与returnsArg组合——第 2 次调用返回传入的参数本身:
const stub = sinon.stub().returns("default"); stub.onSecondCall().returnsArg(0); stub(1); // => "default" 第 1 次:默认 stub(2); // => 2 第 2 次:返回参数 2 stub(3); // => "default" 第 3 次:回退默认与returnsThis组合——第 2 次调用返回this上下文:
const instance = {}; instance.stub = sinon.stub().returns("default"); instance.stub.onSecondCall().returnsThis(); instance.stub(); // => "default" 第 1 次:默认 instance.stub(); // => instance 第 2 次:返回 this instance.stub(); // => "default" 第 3 次:回退默认与throws组合——第 2 次调用抛出指定异常(对测试"第一次成功、第二次开始失败"的重试逻辑尤其有用):
const stub = sinon.stub(); const error = new Error(); stub.onSecondCall().throwsException(error); stub(); // 第 1 次:正常返回 stub(); // 第 2 次:抛出 error其他可组合的方法还包括callsFake(第 N 次调用自定义实现)、resolves/rejects(第 N 次调用返回 Promise 结果)等,完整清单可参考 docs/concepts/stubs/api/index.md 中的 Methods 列表。注意:onCall返回的是行为对象,若需要对其调用withArgs,请始终回到"先withArgs后onCall"的顺序。
底层原理:行为数组、默认行为与回退判定
综合 src/sinon/stub.js 的源码,onCall的完整运行机制可以总结为三层:
- 存储层:每个 stub 实例持有
behaviors数组(索引即调用序次)与defaultBehavior(默认行为)。onCall(index)只是按索引惰性创建并返回行为对象(src/sinon/stub.js); - 取值层:每次实际调用时,
getCurrentBehavior用callCount - 1作为索引取行为,只有行为存在且isPresent()才命中,否则交给getDefaultBehavior回退(src/sinon/stub.js); - 回退层:
getDefaultBehavior的优先级为「stub 自身defaultBehavior→ 父级行为 → 新建空行为」。在withArgs场景下,withArgs创建的是一个独立 fake(见 src/sinon/stub.js),它自带一份defaultBehavior(可通过withArgs(...).returns(...)设置),所以"指定行为用尽"时先回退到 fake 默认、最终才落到 stub 默认——这正是 on-call-2.test.js 中第 3 次callback(FORTY_TWO)返回"Raspberry pie"(stub 默认)的完整链路。
另外需要留意:resetBehavior()会把defaultBehavior置空并清空behaviors数组(src/sinon/stub.js),因此onCall定义的顺序行为在resetBehavior()或reset()之后会全部失效;若需要保留行为定义而只清空调用记录,应使用resetHistory()。详见 docs/concepts/stubs/api/reset-behavior.md 与 docs/concepts/stubs/api/reset-history.md。
实战场景:顺序交互测试三板斧
基于上述能力,onCall族 API 最典型的应用场景包括:
场景一:模拟"首次失败、重试后成功"——用onCall模拟不稳定的下游服务:
const fetchData = sinon.stub(); fetchData.onCall(0).rejects(new Error("network timeout")); // 第 1 次失败 fetchData.onCall(1).resolves({ id: 1 }); // 第 2 次成功 fetchData.resolves({ id: 1 }); // 之后保持成功 // 被测代码:带重试逻辑的数据拉取 await fetchData(); // 抛出 network timeout,触发重试 await fetchData(); // 返回 { id: 1 } await fetchData(); // 返回 { id: 1 }场景二:模拟分页游标——第 1 次返回一页数据、第 2 次返回空页以终止循环:
const listItems = sinon.stub(); listItems.onCall(0).returns([1, 2, 3]); listItems.returns([]); // 后续返回空数组,终止分页循环场景三:验证调用顺序与参数演进——配合withArgs断言"同一次交互中参数随时间变化":
const logger = sinon.stub(); logger.withArgs("info").onFirstCall().returns("level-up");相关文档与源码索引
- 官方 API 文档:docs/concepts/stubs/api/on-call.md、docs/concepts/stubs/api/on-first-call.md、docs/concepts/stubs/api/on-second-call.md、docs/concepts/stubs/api/on-third-call.md
- Stub 全量 API 目录:docs/concepts/stubs/api/index.md
- 文档配套测试用例:docs/tests/docs/stubs/api/on-call-1.test.js、docs/tests/docs/stubs/api/on-call-2.test.js、docs/tests/docs/stubs/api/on-first-call.test.js
- 核心源码实现:src/sinon/stub.js(
onCall/onFirstCall/onSecondCall/onThirdCall、getCurrentBehavior、getDefaultBehavior)、src/sinon/behavior.js(行为对象转发及onCall(...).withArgs(...)报错逻辑) - 核心测试验证:test/src/stub-test.js(顺序序列、链式声明、
withArgs组合与回退)、test/src/behavior-extra-test.js(反模式报错)
小结
stub.onCall(index)以 0 起始的索引精准刻画"第 N 次调用"的行为,配合onFirstCall/onSecondCall/onThirdCall三个别名提升可读性,与withArgs组合后能同时按"参数 + 次序"两个维度定制行为;所有指定行为用尽后自动回退到默认行为,形成一套完整的顺序交互测试方案。使用时牢记两条规则:先withArgs再onCall(反向会抛错),onCall本身不产生行为,行为必须通过returns/throws/callsFake等方法定义。
- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
相关推荐
5步构建专业级音乐体验:洛雪音乐开源音源完整实施指南
5步构建专业级音乐体验:洛雪音乐开源音源完整实施指南 洛雪音乐开源音源项目为音乐爱好者提供了一个完整的音源解决方案,通过科学的音质分级体系和多平台兼容设计,帮助
音视频GoogleTest Mock Actions 详解:掌握测试替身行为控制
GoogleTest Mock Actions 详解:掌握测试替身行为控制 什么是 Mock Actions 在单元测试中,Mock Actions(模拟行为)
测试质量保障开发工具Bootstrap Table 事件系统详解:自定义交互行为的实现方法
Bootstrap Table 事件系统详解:自定义交互行为的实现方法 事件系统基础架构 Bootstrap Table 事件系统(Event System)是
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考