- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
assert.called(spy)是 Sinon 内置断言 API 中最基础的断言之一:只要传入的fake、spy或stub在测试期间至少被调用过一次,断言即通过,否则抛出一个带有详细描述信息的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);关键行为有两点:
- 失败即抛错:断言失败时抛出的异常
name为AssertError(见 assert.js 中fail的实现),可以直接被测试框架捕获并判定用例失败; - 成功静默:断言通过时不返回任何值、不产生异常,配合
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),最终抛出。
与它同族的断言还有calledOnce、calledTwice、calledThrice,它们会额外用%c占位符输出实际的调用次数英文描述(如once、twice、thrice,由timesInWords生成),例如:
expected spy to be called once but was called twice(calledOnce 失败时)
也就是说,assert.called的失败消息聚焦于"从未被调用"这一种失败形态,而次数敏感型断言(如calledOnce)则能进一步告诉你"实际被调用了几次"。
源码视角:assert.called的底层实现
assert.called并不是手写的一个独立函数,而是通过mirrorPropAsAssertion这个工厂函数批量生成的(见 assert.js)。其执行流程如下:
- 校验参数:
verifyIsStub(fake)(assert.js)先确认传入的是有效的 Sinon 代理对象——若传入null会报fake is not a spy,若对象没有getCall方法会报fake is not stubbed; - 校验参数个数:
verifyIsValidAssertion(assert.js)规定called不接受额外参数,多传参数会直接报错called takes 1 argument but was called with N arguments; - 读取布尔属性:对
called而言,meth未提供函数,因此直接读取fake.called这个布尔属性(assert.js); - 判定与输出:
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.notCalled | 与called互斥,失败消息为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上下文、不校验调用次数上限,这些分别由calledWith、calledOn、calledOnce等断言承担,按需组合即可。
进阶实践与注意事项
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实现,支持prefix与includeFail选项)。例如挂到全局后即可写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.called与incrementCallCount)以及同族断言(calledOnce、notCalled、calledWith等)的分工,能帮助你在编写单元测试时快速选出正确的断言,写出既严谨又易读的测试代码。更多断言方法与使用示例,可继续阅读 Assertions API 索引 及仓库中的 断言测试目录。
- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
相关推荐
notebooklm-py 的 Android gRPC 能力与签名取证体系:APK 静态提取、Web 端签名推断与移动端实证验证
notebooklm py 的 Android gRPC 能力与签名取证体系:APK 静态提取、Web 端签名推断与移动端实证验证 本文解析 notebookl
测试开发工具Sinon `assert.calledThrice` 详解:精确断言 spy/fake/stub 恰好被调用三次
Sinon assert.calledThrice 详解:精确断言 spy/fake/stub 恰好被调用三次 sinon.assert.calledThric
测试开发工具Sinon assert.alwaysCalledWith 完全指南:验证 fake/spy/stub 每次调用参数一致
Sinon assert.alwaysCalledWith 完全指南:验证 fake/spy/stub 每次调用参数一致 sinon.assert.always
测试开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考