Sinon 断言指南:`assert.called`——验证 spy、stub 与 fake 至少被调用过一次
2026/9/24 16:26:20 网站建设 项目流程
  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

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

assert.called(spy)是 Sinon 内置断言 API 中最基础的断言之一:只要传入的fakespystub在测试期间至少被调用过一次,断言即通过,否则抛出一个带有详细描述信息的AssertError。本文基于当前仓库中 called.md 文档,并结合 assert.js 源码与对应的 单元测试,从使用方式、错误消息、底层实现到相关断言家族,完整讲解这一断言在测试中的定位与实战用法。

断言签名与语义

assert.called(spy);

语义非常直接:校验目标函数是否被调用过至少一次。只要调用次数 ≥ 1,断言通过;调用次数为 0,断言失败。

它适用于 Sinon 三类测试替身(test double):

  • spy:包装原始函数或独立存在,记录调用信息但不改变行为;
  • stub:可预设行为的替身,同样记录调用信息;
  • fake:更现代、更轻量的独立替身,与 spy 拥有相同的调用记录能力。

因此,凡是能记录调用历史的 Sinon 代理对象,都可以作为assert.called的参数。

快速上手

在 ESM 环境下从sinon包导入 API 后即可使用(参见 called.md 中的基础示例):

import * as sinon from "sinon"; const spy = sinon.spy(); // 此时 spy 尚未被调用,断言失败 sinon.assert.called(spy); // => Error [AssertError]: expected spy to have been called at least once but was never called spy(); // 调用一次 // 现在断言通过,不产生任何异常 sinon.assert.called(spy);

关键行为有两点:

  1. 失败即抛错:断言失败时抛出的异常nameAssertError(见 assert.js 中fail的实现),可以直接被测试框架捕获并判定用例失败;
  2. 成功静默:断言通过时不返回任何值、不产生异常,配合assert.pass的内部机制静默返回(见 assert.js)。

在测试框架中使用

仓库为每个文档 API 都配套了可运行的测试用例。assert.called对应的测试位于 docs/tests/docs/assertions/api/called.test.js,使用tap编写,覆盖了"调用后通过"与"未调用即失败"两个方向:

import tap from "tap"; import * as sinon from "sinon"; tap.test("assert.called - passes when spy was called", (t) => { const spy = sinon.spy(); spy(); t.doesNotThrow(() => { sinon.assert.called(spy); }, "assertion should pass"); t.end(); }); tap.test("assert.called - fails when spy was not called", (t) => { const spy = sinon.spy(); t.throws( () => sinon.assert.called(spy), /expected spy to have been called at least once but was never called/, "assertion should fail with descriptive message" ); t.end(); });

这套测试同时也验证了两件事:失败消息的文本是稳定可匹配的契约(正则中直接匹配了错误文案),以及断言"通过"时确实不会抛出异常。在实际的 Jest / Mocha / Vitest 项目中,可直接把断言放进it/test回调中,失败时框架会自动收集AssertError

失败时的错误消息是如何生成的

assert.called的默认失败消息模板定义在 assert.js:

mirrorPropAsAssertion( "called", "expected %n to have been called at least once but was never called", );

其中%n是格式化占位符,由 spy-formatters.js 中的n处理器替换为spyInstance.toString()(即 spy 的名称描述,如spy)。消息组装完成后,通过(fake.printf || fake.proxy.printf).apply(...)渲染出来(见 assert.js),最终抛出。

与它同族的断言还有calledOncecalledTwicecalledThrice,它们会额外用%c占位符输出实际的调用次数英文描述(如oncetwicethrice,由timesInWords生成),例如:

  • expected spy to be called once but was called twice(calledOnce 失败时)

也就是说,assert.called的失败消息聚焦于"从未被调用"这一种失败形态,而次数敏感型断言(如calledOnce)则能进一步告诉你"实际被调用了几次"。

源码视角:assert.called的底层实现

assert.called并不是手写的一个独立函数,而是通过mirrorPropAsAssertion这个工厂函数批量生成的(见 assert.js)。其执行流程如下:

  1. 校验参数verifyIsStub(fake)(assert.js)先确认传入的是有效的 Sinon 代理对象——若传入null会报fake is not a spy,若对象没有getCall方法会报fake is not stubbed
  2. 校验参数个数verifyIsValidAssertion(assert.js)规定called不接受额外参数,多传参数会直接报错called takes 1 argument but was called with N arguments
  3. 读取布尔属性:对called而言,meth未提供函数,因此直接读取fake.called这个布尔属性(assert.js);
  4. 判定与输出failed为真时调用failAssertion渲染并抛出AssertError,否则调用assert.pass静默通过。

fake.called属性由谁维护

assert.called读到的called布尔属性,是 Sinon 代理对象在每次被调用时incrementCallCount同步更新的(见 proxy-call-util.js):

export function incrementCallCount(proxy) { proxy.called = true; proxy.callCount += 1; proxy.notCalled = false; proxy.calledOnce = proxy.callCount === 1; proxy.calledTwice = proxy.callCount === 2; proxy.calledThrice = proxy.callCount === 3; }

代理在初始化时called被置为false(见 proxy.js),此后每调用一次就置true并递增callCount。因此assert.called本质上是对spy.called === true这一状态的断言封装——你也可以在代码里直接读取spy.called做条件判断,但用assert.called能获得统一、描述清晰的失败消息。

与相关断言的组合使用

assert.called只回答"有没有被调用过"这一个问题。当测试需要更强的约束时,可以按需组合 Assertions API 家族中的其他成员(完整列表见 docs/concepts/assertions/api/):

场景推荐断言语义
至少调用一次assert.called本次讨论的断言
一次都没调用assert.notCalledcalled互斥,失败消息为expected spy to not have been called but was called %c%C
恰好调用一次assert.calledOnce失败时会输出实际次数
恰好两次 / 三次assert.calledTwice/assert.calledThrice次数精确断言
精确参数assert.calledWith/assert.calledWithExactly结合参数校验
以 matcher 匹配参数assert.calledWithMatch结合 matchers 使用
精确调用次数assert.callCount断言具体次数数值

一个常见组合是"先确认被调用,再确认调用参数":

const spy = sinon.spy(); doWork(spy); sinon.assert.called(spy); // 1. 至少调用了一次 sinon.assert.calledWith(spy, "key", 42); // 2. 且最后一次调用参数正确

注意:assert.called本身不校验参数、不校验this上下文、不校验调用次数上限,这些分别由calledWithcalledOncalledOnce等断言承担,按需组合即可。

进阶实践与注意事项

1. 包装真实方法后再断言

除了独立的sinon.spy(),最常见的是对真实对象方法做包装,验证"某个依赖是否被触发":

const service = { save(data) { /* 真实逻辑 */ } }; const saveSpy = sinon.spy(service, "save"); service.save({ id: 1 }); sinon.assert.called(saveSpy); saveSpy.restore(); // 记得恢复原始方法

2. 与 stub 一起使用

stub 同样记录调用信息,因此assert.called也可用于验证"某个被替换的依赖是否被调起":

const stub = sinon.stub().returns(42); compute(stub); sinon.assert.called(stub);

3. 配合assert.expose批量暴露到测试对象

如果希望断言以更贴近阅读习惯的形式出现,可以使用assert.expose把整个断言对象批量挂载到目标对象上(见 assert.js 的expose实现,支持prefixincludeFail选项)。例如挂到全局后即可写assert.called(spy)而无需每次sinon.assert.前缀。

4. 在沙箱(sandbox)中使用

配合sinon.createSandbox(),可以把替身与断言放在同一作用域内统一恢复:

const sandbox = sinon.createSandbox(); const spy = sandbox.spy(); run(); sandbox.assert.called(spy); sandbox.restore();

5. 注意事项

  • 只验证"至少一次":若测试要求"恰好一次",请改用calledOnce,否则调用两次时assert.called依然静默通过,可能掩盖回归;
  • 不接受额外参数assert.called(spy, somethingElse)会直接抛错,而非忽略多余参数;
  • 参数必须是 Sinon 代理:普通函数或非 spy 对象会触发fake is not a spy/fake is not stubbed等前置校验错误;
  • 错误类型为AssertError:断言失败抛出的是名为AssertError的普通Error,可被任何主流测试框架识别;若开启了shouldLimitAssertionLogs选项,长日志会被截断到assertionLogLimit(默认 10K,见 assert.js)。

小结

assert.called是 Sinon 断言体系里最朴素、最常用的一环:它把"目标替身是否被调用过"这一高频测试诉求,封装成一个失败消息清晰、行为可预期的断言。理解它的语义边界(只验证至少一次)、底层数据来源(proxy.calledincrementCallCount)以及同族断言(calledOncenotCalledcalledWith等)的分工,能帮助你在编写单元测试时快速选出正确的断言,写出既严谨又易读的测试代码。更多断言方法与使用示例,可继续阅读 Assertions API 索引 及仓库中的 断言测试目录。

  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载
上一篇:如何在Krita中3分钟实现AI绘画:免费开源的终极创作神器
下一篇:3个资源捕获痛点:猫抓浏览器扩展如何重新定义网页媒体下载体验

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询