测试 this 上下文与 new 调用:sinon-chai 的 calledOn 与 calledWithNew 指南
【免费下载链接】sinon-chaiExtends Chai with assertions for the Sinon.JS mocking framework.项目地址: https://gitcode.com/gh_mirrors/si/sinon-chai
JavaScript 的this指向和new调用,是单元测试里最容易踩坑、也最容易被忽视的两个细节。sinon-chai作为 Chai 生态中广受欢迎的断言插件,把 Sinon.JS 的 spy、stub 能力与 Chai 的自然语言断言合二为一,其中calledOn与calledWithNew正是专门用来验证调用方式的利器。本指南将带你用最短时间掌握 this 上下文测试与 new 调用测试的完整写法,让你的测试代码更严谨、更易读。
为什么单元测试要验证 this 上下文与 new 调用?
先看两个真实场景:
- this 指向错误:某个方法内部依赖
this访问实例数据,调用方却用了解构或单独引用的方式调用,导致this丢失,功能静默失效。 - 忘记 new 调用:构造函数被当成普通函数调用,
this会指向全局对象(严格模式下为undefined),初始化逻辑全部落空,还很难定位。
这类 bug 往往不在"参数对不对"上暴露,因此常规的calledWith断言无能为力。sinon-chai 的 calledOn 断言与 calledWithNew 断言,正是为这两种场景量身定制。
快速上手:安装与初始化 sinon-chai 插件
一行命令即可完成安装:
npm install --save-dev sinon-chai然后在测试入口文件(Mocha 等测试框架的 fixture 中)完成初始化:
import * as chai from "chai"; import sinonChai from "sinon-chai"; chai.use(sinonChai); chai.should(); // 如果打算使用 should 风格如果你希望阅读或修改源码,也可以直接克隆仓库:
git clone https://gitcode.com/gh_mirrors/si/sinon-chaicalledOn 基本用法:断言 this 指向指定对象
calledOn(context)用于验证 spy 在调用时,this是否指向你期望的对象。它同时支持expect与should两种风格:
it("should be called with target as this", function () { const spy = sinon.spy(); const target = { name: "目标对象" }; spy.call(target); // 通过 call 显式指定 this expect(spy).to.have.been.calledOn(target); // ✅ 通过 // spy.should.have.been.calledOn(target); // should 风格同样可用 });几个值得注意的细节:
- 只要有一次满足即可通过:spy 被多次调用时,只要其中任意一次
this正确,calledOn就通过。 - 可以针对单次调用断言:使用
spy.getCall(n)精确检查第 n 次调用,例如spy.getCall(1).should.have.been.calledOn(target)。 - 对 spy、stub、mock 及单个 call 对象全部生效,这也是 sinon-chai 的通用设计。
calledWithNew 用法:断言函数通过 new 调用
calledWithNew用于验证 spy 是否以new关键字调用,是检测"忘记 new"这类问题的首选断言。特别注意:它是属性(property)而非方法,所以后面不要加括号!
it("should be called with new", function () { const spy = sinon.spy(); new spy(); // 通过 new 调用 expect(spy).to.have.been.calledWithNew; // ✅ 正确写法,无括号 // ❌ 错误写法:expect(spy).to.have.been.calledWithNew(); });如果你确实用了new而断言失败,大概率就是多了那一对括号——这是新手最常踩的坑。同样地,当 spy 被多次调用时,只要存在一次new调用,该断言即通过。
always 断言:严格要求每一次调用都符合条件
普通断言只要求"至少一次符合",而always变体要求每一次调用都满足条件,非常适合对行为一致性要求严格的场景:
spy.call(target); spy.call(target); spy.should.always.have.been.calledOn(target); // ✅ 两次 this 都正确new spy(); new spy(); spy.should.always.have.been.calledWithNew; // ✅ 每次都是 new 调用⚠️ 注意always的位置:必须紧跟在should之后,写作should.always.have.been.calledOn(...),而不能写成should.have.been.alwaysCalledOn(...)。
反向断言与错误消息:调试不再迷茫
所有断言都可以用 Chai 的.not取反:
spy.should.not.have.been.calledOn(target); spy.should.not.have.been.calledWithNew;当断言失败时,sinon-chai 会给出清晰的中文可读错误信息,直接告诉你"期望是什么、实际是什么":
| 断言 | 失败时的典型错误消息 |
|---|---|
| calledOn | expected spy to have been called with { } as this, but it was called with ... instead |
| calledWithNew | expected spy to have been called with new |
| always.calledOn | expected spy to always have been called with { } as this, ... |
有了这样的提示,配合getCall(n)逐次排查,定位问题往往只需几秒钟。
两个断言对比速查表
| 对比项 | calledOn(context) | calledWithNew |
|---|---|---|
| 验证目标 | this 上下文指向 | 是否通过 new 调用 |
| 是否带参数 | ✅ 需要传入 context | ❌ 无参数 |
| 是否加括号 | ✅ 方法,带括号 | ❌ 属性,无括号 |
| always 变体 | always.have.been.calledOn(ctx) | always.have.been.calledWithNew |
| 对应实现 | lib/sinon-chai.js第 171-175 行 | lib/sinon-chai.js第 166 行 |
从哪里看源码与测试用例
想深入理解这两个断言的实现与边界行为,可以直接阅读项目源码:
- 核心实现:
lib/sinon-chai.js,其中always标志、断言消息生成逻辑都在此文件中,calledOn与calledWithNew的注册代码清晰可读。 - 测试用例:
test/callContext.js覆盖了 calledOn 的通过、失败、多次调用、always 等全部场景;test/callingWithNew.js则完整验证了 calledWithNew 的行为边界。 - 错误消息断言:
test/messages.js中 "about call context" 与 "about calling with new" 两个小节,展示了断言失败时的精确消息格式。
小结
calledOn与calledWithNew是 sinon-chai 插件中验证调用方式的一对黄金组合:前者守护this上下文,后者拦截"忘记 new"的隐患。记住三个要点即可:
calledOn(target)是方法,需要传参;calledWithNew是属性,不要加括号。- 需要"每一次都满足"时,使用
should.always.have.been.xxx形式。 - 结合
.not取反与getCall(n)单次断言,可覆盖绝大多数边界场景。
把它们用进你的测试套件,JavaScript 中关于调用方式的隐性问题,从此无处遁形。🎯
【免费下载链接】sinon-chaiExtends Chai with assertions for the Sinon.JS mocking framework.项目地址: https://gitcode.com/gh_mirrors/si/sinon-chai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考