news 2026/9/25 12:24:45

Sinon `stub.onCall` 详解:为第 N 次调用定义行为,掌控顺序交互测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sinon `stub.onCall` 详解:为第 N 次调用定义行为,掌控顺序交互测试
  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载

导读

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" 后续所有调用(继续回退到默认)

关键点有三:

  1. 索引从 0 开始:onCall(0)管第一次调用,onCall(1)管第二次调用,依此类推;
  2. 可以跳跃式指定:无需连续铺满每个索引,只声明关心的那几次即可;
  3. 回退机制:一旦声明的指定行为用尽(第 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 argumentFORTY_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的完整运行机制可以总结为三层:

  1. 存储层:每个 stub 实例持有behaviors数组(索引即调用序次)与defaultBehavior(默认行为)。onCall(index)只是按索引惰性创建并返回行为对象(src/sinon/stub.js);
  2. 取值层:每次实际调用时,getCurrentBehavior用callCount - 1作为索引取行为,只有行为存在且isPresent()才命中,否则交给getDefaultBehavior回退(src/sinon/stub.js);
  3. 回退层: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.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载
上一篇:3分钟完成Windows系统激活的智能解决方案:KMS_VL_ALL_AIO完全指南
下一篇:yuzu模拟器:在PC上体验Switch游戏的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 12:24:14

BenchmarkSQL达梦适配版实战:JDBC驱动与TPC-C压测全解析

简介:BenchmarkSQL 是一套开源的数据库性能基准测试工具,这份资源为适配达梦数据库的定制版本。它面向数据库管理员、运维工程师和性能测试人员,主要解决达梦数据库缺乏标准化压测手段的问题,可在接近真实业务的读写混合场景中评估…

作者头像 李华
网站建设 2026/9/25 12:20:22

惠普暗影精灵9拆机清灰后不开机?六大原因与排查指南

1. 一台“本来好好的”暗影精灵9,为什么拆完就翻车惠普暗影精灵9这台机器,在游戏本圈子里保有量相当大,拆机清灰、换硅脂、加硬盘几乎是每个机主早晚都要面对的事。但有个现象特别有意思:很多人机器用了一两年,风扇噪音…

作者头像 李华
网站建设 2026/9/25 12:19:20

Everything + Claude Code入门学习:用ECC打通AI编程代理的文件检索链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 12:15:15

Atlas 300V 24G推理加速卡部署YOLO实战与踩坑指南

如果你最近在折腾 AI 推理服务,或者因为项目需要评估推理硬件,大概率绕不开 Atlas 300V 这个名字。尤其是 Atlas 300V 24G,群里讨论热度一直很高,常看到有人问:这卡到底是不是 GPU?能不能用来部署 YOLO&…

作者头像 李华
网站建设 2026/9/25 12:07:47

工业时序大模型:让AI真正读懂工厂暗数据

1. 项目概述:当大模型真正“看懂”工厂里的每一秒数据流ManuDrive不是又一个挂在PPT上的AI概念,而是我去年在一家汽车零部件厂的产线调试现场,亲眼看着它把三台停机27小时的压铸机重新拉回满负荷运转的真实工具。它不生成诗歌、不写周报、不画…

作者头像 李华
网站建设 2026/9/25 12:05:02

Ethernet/IP调试工具V2.2.0实战:设备发现、CIP报文与UDP广播排查

1. 工业以太网调试为什么绕不开 Ethernet/IP搞自动化或者工控网络的朋友,大概率都经历过这种场景:产线上某台 PLC 跟远程 IO 模块通讯时断时续,交换机指示灯看着正常,Ping 也通,但数据就是偶尔丢包;或者新到…

作者头像 李华