重试机制
您将学习到
- Cypress中命令、查询和断言的区别
- Cypress如何重试多个断言
- 如何延长Cypress重试命令的时间
- Cypress重查询的示例
Cypress帮助测试动态Web应用程序的核心特性是重试机制。就像汽车的良好传动系统一样,它通常在不被察觉的情况下工作。但理解其工作原理将帮助您编写更快速的测试,减少运行时意外。
如果您希望在测试失败时配置重试次数,请查看我们的测试重试指南。
命令、查询和断言
虽然您在Cypress测试中链式调用的所有cy
方法都是命令,但理解它们操作的不同规则很重要。
- 查询会链接起来,整个链会一起重试。
- 断言是一种特殊显示在命令日志中的查询。
- 非查询命令只执行一次。
例如,下面的测试中有5个查询、1个操作和2个断言。
it('creates an item', () => {
// 非查询命令只执行一次。
cy.visit('/')
// .focused()查询和.should()断言链接在一起,
// 重试直到当前聚焦的元素具有'new-todo'类
cy.focused().should('have.class', 'new-todo')
// 查询.get()和.find()
// 链接在一起,形成非查询.type()的主题。
cy.get('.header').find('.new-todo').type('todo A{enter}')
// 两个查询和一个断言链接在一起
cy.get('.todoapp').find('.todo-list li').should('have.length', 1)
})
命令日志显示所有类型的命令,通过的断言显示为绿色。

让我们看看最后的命令链:
cy.get('.todoapp') // 查询
.find('.todo-list li') // 查询
.should('have.length', 1) // 断言
因为在现代Web应用程序中没有任何东西是同步的,Cypress不能查询所有匹配.todo-list li
的DOM元素并检查是否恰好有一个。有很多例子说明这不会很好地工作。
- 如果应用程序在这些命令运行时尚未更新DOM怎么办?
- 如果应用程序在填充DOM元素之前等待其后端响应怎么办?
- 如果应用程序在将结果显示在DOM中之前进行了一些密集计算怎么办?
因此,cy.get
和cy.find()
必须更智能,并期望应用程序可能更新。cy.get()
查询应用程序的DOM,找到匹配选择器的元素,然后将它们传递给.find('.todo-list li')
。.find()
定位一组新元素,并将它们传递给.should()
。.should()
然后对找到的元素列表进行断言(在我们的例子中,它的长度为1)。
- ✅ 如果断言通过,则
.should()
成功完成。 - 🚨 如果断言失败,Cypress将再次查询应用程序的DOM——从链接查询链的顶部开始。它将再次查找匹配
.get().find()
的元素,并重新运行断言。如果断言仍然失败,Cypress将继续重试,直到超时。
重试机制允许测试在断言通过时立即完成每个命令,而无需硬编码等待。如果您的应用程序需要几毫秒甚至几秒钟来渲染每个DOM元素——没问题,测试根本不需要更改。例如,让我们在下面的TodoMVC模型代码中引入3秒的人工延迟来刷新应用程序的UI:
app.TodoModel.prototype.addTodo = function (title) {
this.todos = this.todos.concat({
id: Utils.uuid(),
title: title,
completed: false,
})
// 让我们在3秒后触发UI渲染
setTimeout(() => {
this.inform()
}, 3000)
}
测试仍然通过!cy.get('.todo-list')
立即通过——todo-list
存在——但.should('have.length', 1)
显示旋转指示器,意味着Cypress仍在等待断言通过。

在DOM更新后的几毫秒内,链接的查询.get().find()
定位到一个元素,.should('have.length', 1)
通过。
多个断言
查询和断言总是按顺序执行,并且总是“从顶部”重试。如果您有多个断言,Cypress将在移动到下一个断言之前重试直到每个断言通过。
例如,以下测试有.should()
和.and()
断言。.and()
是.should()
命令的别名,因此第二个断言实际上是一个自定义回调断言,形式为.should(cb)
函数,内部有2个expect
断言。
it('creates two items', () => {
cy.visit('/')
cy.get('.new-todo').type('todo A{enter}')
cy.get('.new-todo').type('todo B{enter}')
cy.get('.todo-list li') // 查询
.should('have.length', 2) // 断言
.and(($li) => {
// .and()断言中的2个mocha断言
expect($li.get(0).textContent, 'first item').to.equal('todo a')
expect($li.get(1).textContent, 'second item').to.equal('todo B')
})
})
因为第一个expect语句(expect($li.get(0).textContent, 'first item').to.equal('todo a')
)失败,第二个语句永远不会到达。.and()
命令在超时后失败,命令日志正确显示第一个遇到的断言should('have.length', 2)
通过,但“first item”断言失败。

隐式断言
通常,Cypress命令具有内置断言,将导致命令重试直到它们通过。例如,.eq()
查询将重试,即使没有任何附加断言,直到找到具有给定索引的元素。
cy.get('.todo-list li') // 查询
.should('have.length', 2) // 断言
.eq(3) // 查询

只有查询可以重试,但大多数其他命令仍然具有内置的_等待_和断言。例如,如.click()的“断言”部分所述,click()
操作命令等待点击,直到元素变为可操作,包括在我们等待时重新运行导致它的查询链,以防页面更新。
Cypress试图像人类用户一样使用浏览器。
- 用户可以点击元素吗?
- 元素是否不可见?
- 元素是否在另一个元素后面?
- 元素是否有
disabled
属性?
操作命令——如.click()
——自动等待直到多个内置断言通过,然后它将尝试一次操作。
超时
默认情况下,每个重试的命令最多重试4秒——defaultCommandTimeout
设置。
增加重试时间
您可以更改所有命令的默认超时。请参阅配置:覆盖选项以获取覆盖此选项的示例。
例如,通过命令行将默认命令超时设置为10秒:
cypress run --config defaultCommandTimeout=10000
我们不建议全局更改命令超时。相反,传递单个命令的{ timeout: ms }
选项以重试不同的时间段。例如:
// 我们修改了影响默认值+添加断言的超时
cy.get('[data-testid="mobile-nav"]', { timeout: 10000 })
.should('be.visible')
.and('contain', 'Home')
Cypress将重试最多10秒以找到具有data-testid
属性mobile-nav
且包含“Home”文本的可见元素。更多示例,请阅读“Cypress简介”指南中的超时部分。
禁用重试
将超时覆盖为0
将基本上禁用重试查询,因为它将花费0毫秒重试。
// 同步检查元素不存在(不重试)
// 例如在服务器端渲染后
cy.get('[data-testid="ssr-error"]', { timeout: 0 }).should('not.exist')
只有查询会重试
任何不是查询的命令,如.click()
,不会“链接在一起”形成后续命令的主题,像查询那样。Cypress将重试任何_导致_命令的查询,并在命令后重试任何断言,但命令本身只执行一次。执行后,导致它们的任何内容都不会重试。
大多数命令不会重试,因为它们可能会改变被测应用程序的状态。例如,Cypress不会重试.click()操作命令,因为它可能会改变应用程序中的某些内容。点击发生后,Cypress也不会重新运行.click()
之前的任何查询。
操作应位于链的末尾,而不是中间
如果以下情况,以下测试可能会有问题:
- 您的JS框架异步重新渲染
- 您的应用程序代码对触发的事件做出反应并移除了元素
错误地链接命令
cy.get('.new-todo')
.type('todo A{enter}') // 操作
.type('todo B{enter}') // 操作后的操作 - 错误
.should('have.class', 'active') // 操作后的断言 - 错误
正确地在操作后结束链
为避免这些问题,最好将上述命令链拆分。
cy.get('.new-todo').type('todo A{enter}')
cy.get('.new-todo').type('todo B{enter}')
cy.get('.new-todo').should('have.class', 'active')
以这种方式编写测试将帮助您避免页面在测试中间重新渲染,Cypress丢失应操作或断言哪些元素的问题。别名——cy.as()
——可以帮助使这种模式不那么突兀。
cy.get('.new-todo').as('new')
cy.get('@new').type('todo A{enter}')
cy.get('@new').type('todo B{enter}')
cy.get('@new').should('have.class', 'active')
极少数情况下,您可能希望重试像.click()
这样的命令。我们描述了一种情况,其中事件监听器仅在延迟后附加到模态弹出窗口,因此导致在.click()
期间触发的默认事件未注册。在这种特殊情况下,您可能希望“继续点击”直到事件注册,对话框消失。在测试何时可以点击?博客文章中阅读相关内容。
由于每个命令中内置的隐式断言,特别是操作命令,您很少需要这种模式。
另一个例子,当确认按钮组件调用click
属性测试与cypress/react挂载库时,以下测试可能有效也可能无效:
错误地检查存根是否被调用
const Clicker = ({ click }) => (
<div>
<button onClick={click}>Click me</button>
</div>
)
it('calls the click prop twice', () => {
const onClick = cy.stub()
cy.mount(<Clicker click={onClick} />)
cy.get('button')
.click()
.click()
.then(() => {
// 在这种情况下有效,但不推荐
// 因为.click()和.then()不重试
expect(onClick).to.be.calledTwice
})
})
如果组件在延迟后调用click
属性,上述示例将失败。
const Clicker = ({ click }) => (
<div>
<button onClick={() => setTimeout(click, 500)}>Click me</button>
</div>
)

测试在组件两 次调用click
属性之前完成,并且没有重试断言expect(onClick).to.be.calledTwice
。
如果React或其他JavaScript库决定在点击之间重新渲染DOM,它也可能失败。
正确等待存根被调用
我们建议使用.as
命令别名存根,并使用cy.get('@alias')
运行断言。
it('calls the click prop', () => {
const onClick = cy.stub().as('clicker')
cy.mount(<Clicker click={onClick} />)
// 良好实践 💡: 不要在命令后链接任何内容
cy.get('button').click()
cy.get('button').click()
// 良好实践 💡: 使用别名引用存根
cy.get('@clicker').should('have.been.calledTwice')
})

使用.should()与回调
如果您正在使用命令,但需要重试整个链,考虑将命令重写为.should(callbackFn)。
下面是一个示例,其中数值在延迟后设置:
<div class="random-number-example">
Random number: <span id="random-number">🎁</span>
</div>
<script>
const el = document.getElementById('random-number')
setTimeout(() => {
el.innerText = Math.floor(Math.random() * 10 + 1)
}, 1500)
</script>

错误地等待值
您可能想编写如下测试,测试数字在1到10之间,尽管这不会按预期工作。测试产生以下值,在注释中注明,然后失败。
// 错误: 此测试不会按预期工作
cy.get('[data-testid="random-number"]') // <div>🎁</div>
.invoke('text') // "🎁"
.then(parseFloat) // NaN
.should('be.gte', 1) // 失败
.and('be.lte', 10) // 从不评估
不幸的是,.then()命令打破了查询链——它之前的任何内容(例如获取元素的文本)都不会重新运行。

正确等待值
我们需要重试获取元素,调用text()
方法,调用parseFloat
函数并运行gte
和lte
断言。我们可以使用.should(callbackFn)
实现这一点。
cy.get('[data-testid="random-number"]').should(($div) => {
// 这里的所有代码将重试
// 直到通过或超时
const n = parseFloat($div.text())
expect(n).to.be.gte(1).and.be.lte(10)
})
上述测试重试获取元素并调用元素的文本以获取数字。当数字最终在应用程序中设置时,gte
和lte
断言通过,测试通过。

另请参阅
- 阅读我们关于对抗测试不稳定的博客文章。
- 您可以向自己的自定义命令和查询添加重试机制。
- 您可以使用第三方插件cypress-pipe和cypress-wait-until重试任何带有附加断言的函数。
- 要了解如何启用Cypress的测试重试功能,重试失败的测试,请查看我们的官方指南测试重试。