Sinon.js Mock 测试速查表 - Sinon spy/stub/mock 测试替身大全
这张速查给写单元测试时想把被测代码与它依赖的环境隔离开的 JS 开发者。内容涵盖用于观察调用的 spy、用固定返回值或抛错或异步结果去替换行为的 stub、带严格期望与 verify 的 mock、用 fake timer 快进 setTimeout 的时间控制,以及一次 restore 全部替身的 sandbox。后面章节补充了 sinon.assert 配套断言与实用模式,比如 sinon.replace 和参数匹配器。读完你应能监听某个依赖、桩掉外部调用、在测试里操控时钟,并在 teardown 时把所有替身一次性复位。
典型使用场景
面向用 Mocha、Jest 等 runner 写 JavaScript/TypeScript 单元测试、需要精确控制依赖行为的开发者。表内的选择逻辑很清晰:Spy 只记录不改行为,用来验证「有没有被调用、参数是什么」;Stub 换掉实现,用来构造目标分支,returns / throws / callsFake / resolves / rejects 分别对应固定返回、抛错、自定义实现、Promise 成功与失败;Mock 把调用与期望绑定,用 expects 声明次数与参数,verify 一次性核对,适合严格的调用契约测试。时间相关逻辑交给 Fake Timer,useFakeTimers 接管 setTimeout 后用 clock.tick 快进,避免测试真的睡几秒。收尾环节是全局卫生:用 createSandbox 在 beforeEach 里统一创建 spy、stub 和假定时器,afterEach 调 sandbox.restore() 一次性还原;单独使用时则必须对每个 spy / stub 手动 restore()。
Spy 间谍 8
sinon.spy(obj, "method")sinon.spy(fn)spy.calledOncespy.calledWith(arg1, arg2)spy.returned(value)spy.callCountspy.args[0]spy.restore()Stub 桩函数 9
sinon.stub(obj, "method")stub.returns(value)stub.throws(Error)stub.callsFake(fn)stub.resolves(value)stub.rejects(Error)stub.callsArg(0)stub.onCall(0).returns(value)stub.restore()Mock 模拟 7
sinon.mock(obj)mock.expects("method")expects.once()expects.withArgs(arg)expects.never()mock.verify()mock.restore()Fake Timer 时间控制 6
sinon.useFakeTimers()clock.tick(1000)clock.runAll()clock.restore()sinon.useFakeTimers({ now: 1000 })sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] })断言与验证 6
sinon.assert.called(spy)sinon.assert.calledOnce(spy)sinon.assert.calledWith(spy, arg)sinon.assert.notCalled(spy)sinon.assert.callCount(spy, 3)sinon.assert.calledWithExactly(spy, arg)Sandbox 沙箱 5
sinon.createSandbox()sandbox.spy(obj, "method")sandbox.stub(obj, "method")sandbox.useFakeTimers()sandbox.restore()实用模式 5
sinon.replace(obj, "method", fn)sinon.replaceGetter(obj, "prop", fn)sinon.replaceSetter(obj, "prop", fn)sinon.fake()sinon.match({ id: sinon.match.number })参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
sinon.spy(obj, "method") | 包住对象方法,保留原行为并记录调用 | sinon.spy(obj, "method") |
sinon.spy(fn) | 把函数包成 spy,统计次数与参数 | sinon.spy(fn) |
spy.calledOnce | 判断是否恰好被调用 1 次 | expect(spy.calledOnce).toBe(true) |
spy.calledWith(a, b) | 判断是否曾以这些参数被调用 | spy.calledWith(1, "x") |
spy.returned(value) | 判断是否曾返回过指定值 | spy.returned(200) |
spy.callCount | 读取累计调用次数 | spy.callCount |
spy.args[0] | 取出第一次调用传入的参数数组 | spy.args[0][0] |
spy.restore() | 把被包装的原方法恢复 | spy.restore() |
sinon.stub(obj, "method") | 替换方法为可编程空函数 | sinon.stub(obj, "method") |
stub.returns(value) | 每次调用返回固定值 | stub.returns(42) |
stub.throws(Error) | 调用时抛出异常,测错误分支 | stub.throws(new Error("boom")) |
stub.callsFake(fn) | 用自定义实现替换原函数 | stub.callsFake(() => 1) |
stub.resolves(value) | 返回 resolved 的 Promise | stub.resolves({ id: 1 }) |
stub.rejects(Error) | 返回 rejected 的 Promise | stub.rejects(new Error("fail")) |
stub.callsArg(0) | 把第一个参数当回调调用,模拟回调风格 API | stub.callsArg(0) |
stub.onCall(0).returns(v) | 按调用次序返回不同值 | stub.onCall(0).returns("a") |
stub.restore() | 恢复被替换的原方法 | stub.restore() |
sinon.mock(obj) | 创建 mock 对象 | const mock = sinon.mock(obj) |
mock.expects("method") | 期望某个方法被调用 | mock.expects("save") |
expects.once() / never() | 期望调用一次 / 期望不被调用 | mock.expects("save").never() |
expects.withArgs(arg) | 期望以指定参数调用 | mock.expects("save").withArgs(1) |
mock.verify() | 验证所有已声明的期望 | mock.verify() |
sinon.useFakeTimers() | 接管 setTimeout / setInterval | sinon.useFakeTimers() |
clock.tick(1000) | 把假时钟快进 1000 毫秒 | clock.tick(1000) |
clock.runAll() | 跑完所有已排队的定时器 | clock.runAll() |
clock.restore() | 恢复真实时间 | clock.restore() |
useFakeTimers({ now }) | 设置假时钟的初始时间 | sinon.useFakeTimers({ now: 1000 }) |
useFakeTimers({ toFake }) | 只伪造指定 API | sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] }) |
sinon.assert.called(spy) | 断言 spy 被调用过 | sinon.assert.called(spy) |
sinon.assert.calledOnce(spy) | 断言恰好调用一次 | sinon.assert.calledOnce(spy) |
sinon.assert.calledWith(spy, a) | 断言调用时传入了该参数 | sinon.assert.calledWith(spy, 1) |
sinon.assert.calledWithExactly(spy, a) | 断言参数严格匹配 | sinon.assert.calledWithExactly(spy, 1) |
sinon.assert.notCalled(spy) | 断言未被调用 | sinon.assert.notCalled(spy) |
sinon.assert.callCount(spy, 3) | 断言调用次数为 3 | sinon.assert.callCount(spy, 3) |
sinon.createSandbox() | 创建独立沙箱统一管理替身 | const sandbox = sinon.createSandbox() |
sandbox.spy / stub / useFakeTimers | 在沙箱中创建 spy、stub 与假定时器 | sandbox.stub(obj, "method") |
sandbox.restore() | 一次性恢复沙箱内所有替身 | sandbox.restore() |
sinon.replace(obj, "m", fn) | 替换方法,推荐写法 | sinon.replace(obj, "method", fn) |
sinon.replaceGetter / replaceSetter | 替换对象 getter / setter | sinon.replaceGetter(obj, "prop", fn) |
sinon.fake() | 创建不改变行为的 fake 函数 | const fake = sinon.fake() |
sinon.match({ }) | 参数匹配器,按类型或结构匹配 | sinon.match({ id: sinon.match.number }) |
易错点与避坑指南
现象某个测试通过后,下一个测试里同名方法行为莫名其妙变了
原因上一个用例里的 stub / spy 没有 restore,原方法被永久替换
处置改用 sinon.createSandbox(),在 afterEach 里统一调 sandbox.restore()
现象测试跑完时间相关逻辑卡住,或后面所有用例都停在等待状态
原因useFakeTimers 接管后没有恢复真实时间,测试进程会一直等假定时器推进
处置在 afterEach 里执行 clock.restore(),或交给 sandbox 统一还原
现象写好 clock.tick(1000) 但回调里的断言没被执行
原因回调内部又注册了新的定时器,或断言放在了 tick 之前的位置
处置用 clock.runAll() 跑完整个队列,并确认断言写在回调触发之后的代码路径上
现象await 一个被 stub 的异步方法后拿到的是 undefined
原因stub 默认返回 undefined 而不是 Promise,await 后取不到值
处置改用 stub.resolves(value) 或 stub.rejects(Error) 来构造明确的成功与失败路径
现象spy.calledWith 为 false,但代码里明明调用了
原因目标是引用类型对象时,按引用比较不会命中
处置用 sinon.match({ id: sinon.match.number }) 这类匹配器,或配合 spy.calledWithMatch 比较
现象回调风格的方法测不到回调体
原因stub 后默认不执行传入的回调函数
处置用 stub.callsArg(0) 让它把第一个参数当回调调用,或用 callsFake 自己手动触发
现象同一个方法在多次调用时需要不同返回值,结果每次都一样
原因stub.returns 是固定的,与调用序号无关
处置改用 stub.onCall(0).returns(...).onCall(1).returns(...) 按次序配置
排障路径
1确认某个方法到底有没有被调用
sinon.assert.called(spy)断言失败时错误信息会直接给出实际调用情况,比手动打印 spy.args 更快定位。
2核对调用次数是否与预期一致
sinon.assert.callCount(spy, 3)次数不符时把 spy.callCount 打出来,和被测代码里的循环、事件绑定次数对照。
3让定时逻辑不靠真实等待
const clock = sinon.useFakeTimers();接管后时间由你控制,配合 clock.tick(1000) 瞬间推进 1000 毫秒。
4跑完所有已排队的定时器
clock.runAll()tick 只推进指定时长,链式调度的定时器可能还没轮到,用 runAll 一次清空。
5构造异步方法的成功返回
stub.resolves({ id: 1 });比 callsFake 包一层 async 函数更简洁,await 出来的就是该值。
6把替身统一交给沙箱管理
sandbox.restore();在 afterEach 里调用一次即可还原全部 spy、stub 与假定时器,杜绝用例间污染。
7按类型或结构匹配复杂参数
sinon.match({ id: sinon.match.number })对象参数按引用比较会失配,用匹配器只校验关心的字段即可。
命令示例
用 sandbox 管理所有替身,afterEach 一次恢复
let sandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})不带 restore 的 stub/spy 会泄漏到后续测试,制造「单跑通过、整体跑失败」的幽灵问题。sandbox 把替身集中登记,afterEach 一行恢复,失败用例的堆栈也更干净。
按调用次序返回不同值,覆盖重试分支
const stub = sandbox.stub(client, "send").onFirstCall().throws(new Error("timeout"))
stub.onSecondCall().returns({ ok: true })
// 第一次调用抛错,第二次返回成功
await service.deliver()onCall(n) 让同一个桩在不同次序返回不同值或抛不同错误,是覆盖「首次失败后重试成功」这类分支的唯一办法——单次 returns/throws 只能表达一种结果。
输出
{ ok: true }假定时器让重试逻辑瞬间跑完
const clock = sandbox.useFakeTimers({ now: 1000 })
const promise = service.retryWithBackoff()
await clock.tickAsync(3000)
await promise真实等待 3 秒的退避重试会让测试套件变慢,还引入时序抖动。假定时器接管 setTimeout/Date 后,tick 一下就把时间推到未来,且 Date 也被固定,结果完全确定。
只桩异步接口,不碰其余定时器
const clock = sandbox.useFakeTimers({
toFake: ["setTimeout", "Date"]
})默认会伪造 requestAnimationFrame、performance 等全部计时 API,动画库或请求库可能因此不工作。用 toFake 白名单只接管需要的 API,能避开这类难以定位的挂起。
用 match 断言复杂参数而不是逐字段硬编码
const spy = sandbox.spy(api, "track")
track("signup")
sinon.assert.calledWith(
spy,
sinon.match({ event: "signup", props: sinon.match.number }),
)calledWith 对对象参数走深比较,加一个新字段就断言失败。sinon.match 只校验关心的键与类型,参数结构演进时断言不会莫名其妙红掉。
输出
AssertError: expected spy to be called with arguments matching
spy(event, props) { ... }常见坑与注意事项
- 忘记 restore 的 stub/spy 会污染同一测试文件里的后续用例,出现「单独跑通过、一起跑失败」的幽灵问题;一律走 sandbox 并在 afterEach 里恢复。
- stub 会丢弃原实现,桩掉一个内部还有副作用的方法(如写库、发消息)会让测试悄悄通过而线上仍出错;只在明确不需要真实行为时使用。
- useFakeTimers 默认伪造 setTimeout/setInterval/Date/requestAnimationFrame/performance,未用 toFake 限定范围时动画库与请求库可能永远不 resolve,测试直接挂到超时。
- 被测代码若在 fake timers 启动前就用 Date.now() 记了时间戳,恢复真实时间后时间线错乱,断言会莫名其妙失败;装替身前先确认创建顺序。
- 对同一个方法 spy 后再 stub 会互相干扰,spy 记录的是桩函数的调用而非真实行为;二者选其一,或用 sandbox.replace 一次性完成替换。
- 测试里硬编码真实时间(如 expect(new Date().getFullYear()))在跨年、跨时区时必然失败;用 useFakeTimers({ now }) 固定时钟再断言。
提示
- Spy 监听调用但不改变行为,Stub 替换行为,Mock 结合两者并验证期望。
- 测试后必须 restore() 恢复原方法,或用 sandbox 自动管理(推荐 afterEach 调 sandbox.restore)。
- Fake Timer 用于测试定时任务,避免真实等待,记得 tick 后 restore。
- sinon.assert 抛出错误,适合在测试框架中使用;也可直接读取 spy 属性断言。
- stub.resolves/rejects 处理 Promise,比 callsFake 包装 async 函数更简洁。
常见问题
spy、stub、mock 都能"改写对象",到底该用哪一个?
按「要不要改变行为、要不要写死期望」分。sinon.spy(obj, "method") 包住原方法但保留原行为,只记录调用,之后读 calledOnce、calledWith(arg1, arg2)、returned(value)、callCount、args[0] 即可,适合验证「有没有被正确调用」。sinon.stub(obj, "method") 把方法替换成可编程的空函数,原行为被丢弃,再用 returns / throws / resolves / rejects 决定返回什么,适合隔离依赖。sinon.mock(obj) 则是把两者合一:先 expects("method") 声明调用次数(once、never)与参数,最后 mock.verify() 统一校验,语法更重,只在期望密集时值得。
为什么测试跑完其它用例开始莫名失败?
典型原因是替身没有还原,全局污染了后续用例。本表给出的正解是沙箱:在 beforeEach 里 sinon.createSandbox(),之后一律用 sandbox.spy / sandbox.stub / sandbox.useFakeTimers 创建替身,afterEach 里调 sandbox.restore() 一次性还原全部 spy、stub 和假定时器。手动方式则是每个 spy/stub 单独调 restore()。fake timer 尤其容易漏:clock.tick(1000) 快进之后如果不 clock.restore(),后面所有依赖真实时间的逻辑都会停在原地。判断依据很直接:如果失败用例依赖某个全局对象(Date、setTimeout、被替换的方法),就先去查前一个用例是否遗留了替身。
测定时器逻辑时不想真等三秒,该怎么用假定时器?
用 sinon.useFakeTimers() 接管 setTimeout 和 setInterval,然后靠 clock 操作虚拟时间:clock.tick(1000) 快进 1000 毫秒,只触发这段时间内到期的回调;clock.runAll() 一次跑完所有已排队的定时器,适合把循环调度一次跑干净。要固定起点时间就用 sinon.useFakeTimers({ now: 1000 });只想伪造部分 API 以免影响其它代码,用 sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] }) 精确指定。测完必须 clock.restore(),或者交给沙箱统一还原。整套下来毫秒级完成,且断言的是回调真正执行后的状态。
异步方法用 callsFake 包装还是 resolves/rejects?
优先用 resolves 和 rejects:stub.resolves(value) 直接返回一个 resolved 的 Promise,stub.rejects(Error) 返回 rejected 的 Promise,专测异步成功与失败路径,比用 callsFake 手写一个 async 函数再 await 要短得多。只有当被替换的方法不是返回 Promise、或者你需要保留复杂多步逻辑时,才用 stub.callsFake(fn) 提供自定义实现。旧式回调风格 API 则用 stub.callsArg(0) 触发传入的第一个参数当作回调。同一方法的多次调用需要不同结果时,用 stub.onCall(0).returns(value) 按调用次序配置。
mock.expects 校验失败时报错很笼统,怎么定位?
先确认三件事:期望是不是真没被调用(expects.never() 被违反),还是调用次数对了但参数对不上(expects.withArgs(arg) 不匹配)——后者应换成更宽的 sinon.match({ id: sinon.match.number }) 这类按类型或结构匹配的匹配器,只在关键字段上收紧。还要检查传入值是不是被前面的测试替身污染了。若根本不需要严格的「期望式」校验,mock 的语法负担不划算,改用 spy 加断言更直接:sinon.assert.called(spy)、sinon.assert.calledOnce(spy)、sinon.assert.calledWith(spy, arg)、sinon.assert.notCalled(spy)、sinon.assert.callCount(spy, 3)、sinon.assert.calledWithExactly(spy, arg),失败信息会指向具体的调用记录。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。