Stubs, Spies 和 Clocks
你将学到
- 如何使用
cy.stub()
、cy.spy()
和cy.clock()
- 使用 stubs、spies 和 clocks 的常见场景
- 如何对 stubs 和 spies 进行断言
Cypress 内置了通过 cy.stub()
和 cy.spy()
进行桩函数和监视的能力,或者使用 cy.clock()
修改应用时间 - 这让你可以操控 Date
、setTimeout
、clearTimeout
、setInterval
或 clearInterval
。
这些命令在编写单元测试和集成测试时都非常有用。
库和工具
Cypress 自动打包并封装了这些库:
名称 | 功能 |
---|---|
sinon | 提供 cy.stub() 和 cy.spy() API |
lolex | 提供 cy.clock() 和 cy.tick() API |
sinon-chai | 为 stubs 和 spies 添加 chai 断言支持 |
你可以参考这些库的文档获取更多示例和解释。
常见场景
Stubs
Stub 是一种修改函数并将其行为控制权交给开发者(程序员)的方式。
Stub 最常用于单元测试,但在某些集成/e2e测试中仍然有用。
// 创建一个独立的 stub (通常用于单元测试)
cy.stub()
// 用 stub 函数替换 obj.method()
cy.stub(obj, 'method')
// 强制 obj.method() 返回 "foo"
cy.stub(obj, 'method').returns('foo')
// 当使用 "bar" 参数调用时,强制 obj.method() 返回 "foo"
cy.stub(obj, 'method').withArgs('bar').returns('foo')
// 强制 obj.method() 返回解析为 "foo" 的 Promise
cy.stub(obj, 'method').resolves('foo')
// 强制 obj.method() 返回被错误拒绝的 Promise
cy.stub(obj, 'method').rejects(new Error('foo'))
通常在函数有副作用需要控制时使用 stub。
常见场景:
- 你有一个接受回调的函数,并希望调用该回调
- 你的函数返回一个
Promise
,你想自动解析或拒绝它 - 你有一个包装
window.location
的函数,不希望应用被导航 - 你试图通过强制失败来测试应用的"失败路径"
- 你试图通过强制成功来测试应用的"成功路径"
- 你想"欺骗"应用使其认为已登录或已登出
- 你使用
oauth
并想 stub 登录方法
Spies
Spy 让你能够"监视"函数,通过捕获并断言函数是否以正确的参数被调用,或者函数被调用了特定次数,甚至函数的返回值或调用上下文是什么。
Spy 不会修改函数的行为 - 它完全保持原样。当测试多个函数之间的契约且不关心真实函数可能产生的副作用时,Spy 最有用。
cy.spy(obj, 'method')
Clock
在某些情况下,控制应用的 date
和 time
很有用,可以覆盖其行为或避免缓慢的测试。
使用 cy.clock() 你可以控制:
Date
setTimeout
setInterval
常见场景
控制 setInterval
- 你在应用中用
setInterval
轮询某些内容并想控制它 - 你有节流或防抖函数需要控制
启用 cy.clock()
后,你可以通过推进毫秒数来控制时间。
cy.clock()
cy.visit('http://localhost:3333')
cy.get('#search').type('Acme Company')
cy.tick(1000)
你可以在访问应用前调用 cy.clock()
,我们会在下次 cy.visit()
时自动将其绑定到应用。同样的概念适用于使用 cy.mount()
挂载组件。我们在你的代码中的任何计时器被调用之前绑定。
恢复时钟
你可以恢复时钟,让应用在没有时间相关全局函数操控的情况下正常运行。这在测试之间会自动调用。
cy.clock()
cy.visit('http://localhost:3333')
cy.get('#search').type('Acme Company')
cy.tick(1000)
// 更多测试代码
// 恢复时钟
cy.clock().then((clock) => {
clock.restore()
})
// 更多测试代码
你也可以使用 .invoke() 调用 restore
函数来恢复。
cy.clock().invoke('restore')
断言
一旦有了 stub
或 spy
,你就可以对它们进行断言。
const user = {
getName: (arg) => {
return arg
},
updateEmail: (arg) => {
return arg
},
fail: () => {
throw new Error('fail whale')
},
}
// 强制 user.getName() 返回 "Jane"
cy.stub(user, 'getName').returns('Jane Lane')
// 监视 updateEmail 但不改变其行为
cy.spy(user, 'updateEmail')
// 监视 fail 但不改变其行为
cy.spy(user, 'fail')
// 调用 getName
const name = user.getName(123)
// 调用 updateEmail
const email = user.updateEmail('jane@devs.com')
try {
// 调用 fail
user.fail()
} catch (e) {}
expect(name).to.eq('Jane Lane') // true
expect(user.getName).to.be.calledOnce // true
expect(user.getName).not.to.be.calledTwice // true
expect(user.getName).to.be.calledWith(123)
expect(user.getName).to.be.calledWithExactly(123) // true
expect(user.getName).to.be.calledOn(user) // true
expect(email).to.eq('jane@devs.com') // true
expect(user.updateEmail).to.be.calledWith('jane@devs.com') // true
expect(user.updateEmail).to.have.returned('jane@devs.com') // true
expect(user.fail).to.have.thrown('Error') // true
集成与扩展
除了将这些工具集成在一起,我们还改进和扩展了这些工具之间的协作。
一些例子:
- 我们替换了 Sinon 的参数字符串化器,使用了一个更简洁、性能更好的自定义版本
- 我们改进了
sinon-chai
断言输出,改变了通过和失败测试时的显示内容 - 我们为
stub
和spy
API 添加了别名支持 - 我们在测试之间自动恢复和清理
stub
、spy
和clock
我们还将所有这些 API 直接集成到命令日志中,这样你可以直观地看到应用中发生了什么。
我们会在以下情况时进行视觉提示:
stub
被调用时spy
被调用时clock
被推进时
当你使用 .as()
命令设置别名时,我们也会将这些别名与调用关联起来。这与别名 cy.intercept()
的工作方式相同。
当 stub 通过调用 .withArgs(...)
方法创建时,我们也会将它们视觉上链接在一起。
当你点击 stub 或 spy 时,我们还会输出极其有用的调试信息。
例如,我们会自动显示:
- 调用计数(以及总调用次数)
- 参数,未经转换(它们是真实参数)
- 函数的返回值
- 函数被调用时的上下文