Skip to main content
Cypress应用

Stubs, Spies 和 Clocks

info
你将学到
  • 如何使用 cy.stub()cy.spy()cy.clock()
  • 使用 stubs、spies 和 clocks 的常见场景
  • 如何对 stubs 和 spies 进行断言

Cypress 内置了通过 cy.stub()cy.spy() 进行桩函数和监视的能力,或者使用 cy.clock() 修改应用时间 - 这让你可以操控 DatesetTimeoutclearTimeoutsetIntervalclearInterval

这些命令在编写单元测试集成测试时都非常有用。

库和工具

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

在某些情况下,控制应用的 datetime 很有用,可以覆盖其行为或避免缓慢的测试。

使用 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')

断言

一旦有了 stubspy,你就可以对它们进行断言。

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 断言输出,改变了通过和失败测试时的显示内容
  • 我们为 stubspy API 添加了别名支持
  • 我们在测试之间自动恢复和清理 stubspyclock

我们还将所有这些 API 直接集成到命令日志中,这样你可以直观地看到应用中发生了什么。

我们会在以下情况时进行视觉提示:

  • stub 被调用时
  • spy 被调用时
  • clock 被推进时

当你使用 .as() 命令设置别名时,我们也会将这些别名与调用关联起来。这与别名 cy.intercept() 的工作方式相同。

当 stub 通过调用 .withArgs(...) 方法创建时,我们也会将它们视觉上链接在一起。

当你点击 stub 或 spy 时,我们还会输出极其有用的调试信息。

例如,我们会自动显示:

  • 调用计数(以及总调用次数)
  • 参数,未经转换(它们是真实参数)
  • 函数的返回值
  • 函数被调用时的上下文

另请参阅