最佳实践
你将学到
- 如何组织测试、登录和控制状态的最佳实践
- 选择元素和分配返回值的策略
- 访问外部网站的最佳实践
- 如何避免依赖先前测试的状态
- 何时使用
after
或afterEach
钩子 - 如何避免测试中不必要的等待
- 为测试设置全局基础URL以节省时间
Cypress团队维护了真实世界应用(RWA),这是一个完整的示例应用程序,展示了在实际和现实场景中使用Cypress的最佳实践和可扩展策略。
RWA实现了完整的代码覆盖率,包括跨多个浏览器和设备尺寸的端到端测试,还包括视觉回归测试、API测试、单元测试,并在高效的CI管道中运行所有测试。
该应用程序捆绑了你所需的一切,只需克隆仓库即可开始测试。
组织测试、登录、控制状态
反模式: 共享页面对象,使用UI登录,不采取快捷方式。
最佳实践: 隔离测试规范,以编程方式登录应用程序,并控制应用程序的状态。
我们在AssertJS(2018年2月)上做了一个“最佳实践”的会议演讲。这个视频演示了如何分解应用程序和组织测试。
AssertJS - Cypress最佳实践我们在示例中有几个登录配方。
选择元素
反模式: 使用高度脆弱的、容易变化的选择器。
最佳实践: 使用 data-*
属性为选择器提供上下文,并将其与CSS或JS更改隔离。
你编写的每个测试都将包括对元素的选择器。为了避免许多麻烦,你应该编写对更改具有弹性的选择器。
通常,用户会遇到以下问题:
- 应用程序可能使用动态类或ID,这些会变化
- 选择器因CSS样式或JS行为的开发更改而中断
幸运的是,可以避免这两个问题。
- 不要基于CSS属性(如:
id
、class
、tag
)定位元素 - 不要定位可能更改其
textContent
的元素 - 添加
data-*
属性以更容易定位元素
工作原理
给定一个我们想要交互的按钮:
<button
id="main"
class="btn btn-large"
name="submission"
role="button"
data-cy="submit"
>
Submit
</button>
让我们研究如何定位它:
选择器 | 推荐 | 说明 |
---|---|---|
cy.get('button').click() | 从不 | 最差 - 太通用,没有上下文。 |
cy.get('.btn.btn-large').click() | 从不 | 差。与样式耦合。极易变化。 |
cy.get('#main').click() | 偶尔 | 较好。但仍与样式或JS事件监听器耦合。 |
cy.get('[name="submission"]').click() | 偶尔 | 与具有HTML语义的 name 属性耦合。 |
cy.contains('Submit').click() | 视情况 | 更好。但仍与可能变化的文本内容耦合。 |
cy.get('[data-cy="submit"]').click() | 总是 | 最佳。与所有更改隔离。 |
通过 tag
、class
或 id
定位上述元素非常不稳定且极易变化。你可能更换元素,重构CSS并更新ID,或添加或删除影响元素样式的类。
相反,向元素添加 data-cy
属性为我们提供了一个仅用于测试的目标选择器。
data-cy
属性不会因CSS样式或JS行为更改而变化,这意味着它不与元素的行为或样式耦合。
此外,它向所有人明确表示此元素直接由测试代码使用。
真实示例
真实世界应用(RWA) 使用了两个有用的自定义命令来为测试选择元素:
getBySel
生成具有data-test
属性的元素,该属性匹配指定的选择器。getBySelLike
生成具有data-test
属性的元素,该属性包含指定的选择器。
// cypress/support/commands.ts
Cypress.Commands.add('getBySel', (selector, ...args) => {
return cy.get(`[data-test=${selector}]`, ...args)
})
Cypress.Commands.add('getBySelLike', (selector, ...args) => {
return cy.get(`[data-test*=${selector}]`, ...args)
})
文本内容
在阅读上述规则后,你可能会想:
如果应该总是使用数据属性,那么何时使用
cy.contains()
?
一个经验法则是问自己:
如果元素的内容更改,你希望测试失败吗?
- 如果答案是肯定的:则使用
cy.contains()
- 如果答案是否定的:则使用数据属性。
示例:
如果我们再次查看按钮的 <html>
...
<button id="main" class="btn btn-large" data-cy="submit">Submit</button>
问题是:Submit
文本内容对你的测试有多重要?如果文本从 Submit
更改为 Save
- 你希望测试失败吗?
如果答案是是,因为 Submit
这个词至关重要且不应更改 - 则使用 cy.contains()
定位元素。这样,如果更改,测试将失败。
如果答案是否,因为文本可以更改 - 则使用 cy.get()
和数据属性。将文本更改为 Save
不会导致测试失败。
Cypress 和 Testing Library
Cypress 喜欢 Testing Library 项目。我们在内部使用 Testing Library,我们的理念与 Testing Library 的宗旨和测试编写方法紧密一致。我们强烈支持他们的最佳实践,适用于像 cy.contains()
这样的情况,你希望如果特定内容或可访问角色不存在时测试失败。
你可以使用 Cypress Testing Library 包来使用熟悉的测试库方法(如 findByRole
、findByLabelText
等)在 Cypress 规范中选择元素。
如果你来自 React Testing Library 背景,并寻找更多资源来理解我们建议你如何测试组件,请查看:Cypress 组件测试。
可访问性测试
使用数据属性、文本内容或 Testing Library 定位器选择元素可能对可访问性有不同的影响,但这些方法都不是“完整”的可访问性测试,你总是需要额外的、特定于可访问性的测试(包括自动化和手动测试)来确认你的应用程序对残疾人和他们使用的技术是否按预期工 作。详见我们的可访问性测试指南以获取更多详细信息和方法的比较。
分配返回值
反模式: 尝试使 用 const
、let
或 var
分配命令的返回值。
最佳实践: 使用 别名和闭包来访问和存储 命令生成的内容。
许多初次使用 Cypress 的用户会认为它的代码是同步运行的。
我们经常看到新用户编写如下代码:
// 不要这样做。它不会 按你想象的方式工作。
const a = cy.get('a')
cy.visit('https://example.cypress.io')
// 不,会失败
a.first().click()
// 相反,这样做。
cy.get('a').as('links')
cy.get('@links').first().click()
在 Cypress 中很少需要使用 const
、let
或 var
。如果你在使用它们,你可能需要进行一些重构。
如果你是 Cypress 的新手,并希望更好地理解命令的工作原理 - 请阅读我们的 Cypress 入门指南。
如果你已经熟悉 Cypress 命令,但发现自己使用 const
、let
或 var
,那么你通常是在尝试做以下两件事之一:
- 你正在尝试存储和比较值,如文本、类、属性。
- 你正在尝试在测试和钩子(如
before
和beforeEach
)之间共享值。
对于这些模式,请阅读我们的变量和别名指南。
访问外部网站
反模式: 尝试访问或与你无法控制的站点或服务器交互。
最佳实践: 仅测试你控制的网站。尽量避免访问或依赖第三方服务器。如果选择,可以使用 cy.request()
通过其API与第三方服务器通信。如果可能,通过 cy.session()
缓存结果以避免重复访问。另请参阅反对测试你无法控制的应用程序的原 因。
我们的许多用户首先尝试做的事情之一就是在他们的测试中涉及第三方服务器或服务。
你可能希望在几种情况下访问第三方服务:
- 当你的应用程序通过 OAuth 使用另一个提供商时测试登录。
- 验证你的服务器是否更新了第三方服务器。
- 检查你的电子邮件以查看服务器是否发送了“忘记密码”电子邮件。
如果选择,这些情况可以通过 cy.visit()
和 cy.origin()
进行测试。但是,你只希望将这些命令用于你控制的资源,无论是通过控制域还是托管实例。这些用例通常适用于:
- 作为服务平台的身份验证,如 Auth0、Okta、Microsoft、AWS Cognito 等,通过用户名/密码认证。这些域和服务实例通常由你或你的组织拥有和控制。
- CMS 实例,如 Contentful 或 Wordpress 实例。
- 其他类型的服务,位于你控制的域下。
使用社交平台认证的潜在挑战
其他服务,如通过流行媒体提供商的社交登录,不建议使用。测试社交登录可能有效,尤其是在本地运行时。然而,我们认为这是一种不好的做法,不建议这样做,因为:
- 它非常耗时,并减慢你的测试速度(除非使用
cy.session()
)。 - 第三方站点可能已更改或更新其内容。
- 第三方站点可能在你无法控制的情况下出现问题。
- 第三方站点可能检测到你正在通过脚本进行测 试并阻止你。
- 第三方站点可能有禁止自动化登录的政策,导致账户被封禁。
- 第三方站点可能检测到你是一个机器人,并提供诸如双因素认证、验证码等机制来防止自动化。这在持续集成平台和一般自动化中很常见。
- 第三方站点可能正在运行 A/B 测试。
让我们看看处理这些情况的几种策略。
登录时
许多 OAuth 提供商,尤其是社交登录,运行 A/B 实验,这意味着他们的登录屏幕是动态变化的。这使得自动化测试变得困难。
许多 OAuth 提供商还会限制你可以向他们发出的网络请求数量。例如,如果你尝试测试 Google,Google 会自动检测到你不是一个人类,而不是给你一个 OAuth 登录屏幕,他们会让你填写验证码。
此外,通过 OAuth 提供商的测试是可变的 - 你首先需要他们在服务上的真实用户,然后对该用户的任何修改可能会影响下游的其他测试。
以下是你可以选择用来缓解这些问题的解决方案:
- 使用你控制的另一个平台通过
cy.origin()
使用用户名和密码登录。这很可能保证你不会遇到上述问题,同时仍然能够自动化你的登录流程。你可以通过使用cy.session()
来减少认证请求的数量。 - 如果
cy.origin()
不可用,stub 出 OAuth 提供商并使用其 UI 完全绕过它。你可以欺骗你的应用程序,使其相信 OAuth 提供商已将其令牌传递给你的应用程序。 - 如果必须获取真实令牌且
cy.origin()
不可用,可以使用cy.request()
并使用你的 OAuth 提供商提供的编程 API。这些 API 可能更少变化,并且你可以避免诸如限制和 A/B 测试等问题。 - 除了让你的测试代码绕过 OAuth,你也可以请求你的服务器帮助。也许 OAuth 令牌所做的只是在你的数据库中生成一个用户。通常 OAuth 仅在最初有用,你的服务器与客户端建立自己的会话。如果是这种情况,使用
cy.request()
直接从你的服务器获取会话,如果cy.origin()
不可用,则完全绕过提供商。
第三方服务器
有时你在应用程序中采取的操作可能影响另一个第三方应用程序。这些情况并不常见,但有可能。想象你的应用程序与 GitHub 集成,通过使用你的应用程序,你可以更改 GitHub 内部的数据。
运行测试后,不要尝试 cy.visit()
GitHub,你可以使用 cy.request()
直接与 GitHub 的 API 进行编程交互。
这避免了需要接触另一个应用程序的 UI。
验证发送的电子邮件
通常,在用户注册或忘记密码等场景中,你的服务器会安排发送电子邮件。
- 如果你的应用程序在本地运行并通过 SMTP 服务器直接发送电子邮件,你可以在 Cypress 内部使用临时本地测试 SMTP 服务器。阅读博客文章 "使用 Cypress 测试 HTML 电子邮件" 以获取详细信息。
- 如果你的应用程序使用第三方电子邮件服务,或者你无法 stub SMTP 请求,你可以使用具有 API 访问权限的测试电子邮件收件箱。阅读博客文章 "使用 SendGrid 和 Ethereal 账户全面测试 HTML 电子邮件" 以获取详细信息。
Cypress 甚至可以在其浏览器中加载接收到的 HTML 电子邮件,以验证电子邮件的功能和视觉样式:

- 在其他情况下,你应该尝试使用
cy.request()
命令查询服务器上的端点,该端点告诉你哪些电子邮件已排队或已发送。这将为你提供一种编程方式,无需涉及 UI 即可知道。你的服务器需要公开此端点。 - 你也可以使用
cy.request()
到第三方电子邮件接收服务器,该服务器公开了读取电子邮件的 API。你将需要适当的认证凭据,你的服务器可以提供,或者你可以使用环境变量。一些电子邮件服务已经提供了Cypress插件来访问电子邮件。
测试依赖于先前测试的状态
反模式: 将多个测试耦合在一起。
最佳实践: 测试应始终能够独立于彼此运行并且仍然通过。
你只需要做一件事就可以知道你是否错误地耦合了测试,或者一个测试是否依赖于前一个测试的状态。
将 it
更改为 it.only
并刷新浏览器。
如果这个测试可以单独运行并通过 - 恭喜你写了一个好的测试。
如果不是这样,那么你应该重构并改变你的方法。
如何解决这个问题:
- 将先前测试中的重复代码移动到
before
或beforeEach
钩子中。 - 将多个测试合并为一个更大的测试。
让我们想象以下填写表单的测试。
- 端到端测试
- 组件测试
// 一个不应该做的例子
describe('我的表单', () => {
it('访问表单', () => {
cy.visit('/users/new')
})
it('需要名字', () => {
cy.get('[data-testid="first-name"]').type('Johnny')
})
it('需要姓氏', () => {
cy.get('[data-testid="last-name"]').type('Appleseed')
})
it('可以提交有效表单', () => {
cy.get('form').submit()
})
})
// 一个不应该做的例子
describe('我的表单', () => {
it('访问表单', () => {
cy.mount(<UserForm />)
})
it('需要名字', () => {
cy.get('[data-testid="first-name"]').type('Johnny')
})
it('需要姓氏', () => {
cy.get('[data-testid="last-name"]').type('Appleseed')
})
it('可以提交有效表单', () => {
cy.get('form').submit()
})
})
上面的测试有什么问题?它们都耦合在一起了!
如果你将 it
更改为 it.only
在任何最后三个测试上,它们都会失败。每个测试都需要前一个测试以特定顺序运行才能通过。
以下是我们可以解决这个问题的两种方法:
1. 合并为一个测试
- 端到端测试
- 组件测试
// 更好一些
describe('我的表单', () => {
it('可以提交有效表单', () => {
cy.visit('/users/new')
cy.log('填写名字') // 如果你真的需要这个
cy.get('[data-testid="first-name"]').type('Johnny')
cy.log('填写姓氏') // 如果你真的需要这个
cy.get('[data-testid="last-name"]').type('Appleseed')
cy.log('提交表单') // 如果你真的需要这个
cy.get('form').submit()
})
})
// 更好一些
describe('我的表单', () => {
it('可以提交有效表单', () => {
cy.mount(<NewUser />)
cy.log('填写名字') // 如果你真的需要这个
cy.get('[data-testid="first-name"]').type('Johnny')
cy.log('填写姓氏') // 如果你真的需要这个
cy.get('[data-testid="last-name"]').type('Appleseed')
cy.log('提交表单') // 如果你真的需要这个
cy.get('form').submit()
})
})
现在我们可以在这个测试上放一个 .only
,它将成功运行,而不依赖于任何其他测试。理想的 Cypress 工作流程是一次编写和迭代一个测试。
2. 在每个测试之前运行共享代码
- 端到端测试
- 组件测试
describe('我的表单', () => {
beforeEach(() => {
cy.visit('/users/new')
cy.get('[data-testid="first-name"]').type('Johnny')
cy.get('[data-testid="last-name"]').type('Appleseed')
})
it('显示表单验证', () => {
// 清除名字
cy.get('[data-testid="first-name"]').clear()
cy.get('form').submit()
cy.get('[data-testid="errors"]').should('contain', '名字是必填项')
})
it('可以提交有效表单', () => {
cy.get('form').submit()
})
})
describe('我的表单', () => {
beforeEach(() => {
cy.mount(<NewUser />)
cy.get('[data-testid="first-name"]').type('Johnny')
cy.get('[data-testid="last-name"]').type('Appleseed')
})
it('显示表单验证', () => {
// 清除名字
cy.get('[data-testid="first-name"]').clear()
cy.get('form').submit()
cy.get('[data-testid="errors"]').should('contain', '名字是必填项')
})
it('可以提交有效表单', () => {
cy.get('form').submit()
})
})
上面的例子是理想的,因为现在我们正在在每个测试之间重置状态,并确保先前测试中的任何内容不会泄漏到后续测试中。
我们还在为针对表单“默认”状态编写多个测试铺平道路。这样每个测试保持精简,但每个测试可以独立运行并通过。
创建“微小”测试与单一断言 仅限端到端测试
反模式: 表现得像你在编写单元测试。
最佳实践: 添加多个断言,不用担心
我们见过许多用户编写这种代码:
describe('我的表单', () => {
beforeEach(() => {
cy.visit('/users/new')
cy.get('[data-testid="first-name"]').type('johnny')
})
it('有验证属性', () => {
cy.get('[data-testid="first-name"]').should(
'have.attr',
'data-validation',
'required'
)
})
it('有活动类', () => {
cy.get('[data-testid="first-name"]').should('have.class', 'active')
})
it('有格式化的名字', () => {
cy.get('[data-testid="first-name"]')
// 首字母大写
.should('have.value', 'Johnny')
})
})
虽然技术上这运行良好 - 但这真的过度了,而且性能不好。
为什么你在组件和单元测试中这样做:
- 当断言失败时,你依赖测试标题来知道什么失败了
- 你被告知添加多个断言是不好的,并接受了这一点
- 拆分多个测试没有性能损失,因为它们运行得非常快
为什么你不应该在端到端测试中这样做:
- 编写集成测试与单元测试 不同
- 你将始终知道(并且可以直观地看到)大型测试中哪个断言失败
- Cypress 运行一系列异步生命周期事件,在测试之间重置状态
- 重置测试比添加更多断言要慢得多
在 Cypress 中,测试发出 30 多个命令是很常见的。因为几乎每个命令都有一个隐式断言(因此可能失败),即使限制你的断言,你也没有节省任何东西,因为任何单个命令都可能隐式失败。
你应该如何重写这些测试:
describe('我的表单', () => {
beforeEach(() => {
cy.visit('/users/new')
})
it('验证并格式化名字', () => {
cy.get('[data-testid="first-name"]')
.type('johnny')
.should('have.attr', 'data-validation', 'required')
.and('have.class', 'active')
.and('have.value', 'Johnny')
})
})
使用 after
或 afterEach
钩子
反模式: 使用 after
或 afterEach
钩子来清理状态。
最佳实践: 在测试运行之前清理状态。
我们看到许多用户将代码添加到 after
或 afterEach
钩子中,以清理当前测试生成的状态。
我们最常见的是看到这样的测试代码:
describe('登录用户', () => {
beforeEach(() => {
cy.login()
})
afterEach(() => {
cy.logout()
})
it('测试', ...)
it('更多', ...)
it('事情', ...)
})
让我们看看为什么这并不真正必要。
悬空状态是你的朋友
Cypress 的最佳部分之一是其对可调试性的重视。与其他测试工具不同 - 当你的测试结束时 - 你会在测试完成的确切点留下你的工作应用程序。
这是一个绝佳的机会,让你在测试结束的状态下使 用你的应用程序!这使你能够编写部分测试,逐步驱动你的应用程序,同时编写测试和应用程序代码。
我们构建 Cypress 来支持这种用例。事实上,Cypress 不会在测试结束时清理其内部状态。我们希望你在测试结束时拥有悬空状态!诸如 stubs、spies,甚至 intercepts 的东西在测试结束时不会被移除。这意味着你的应用程序在运行 Cypress 命令或你在测试结束后手动操作时的行为是相同的。
如果你在每个测试后移除应用程序的状态,那么你立即失去了在这种模式下使用应用程序的能力。在结束时注销总是会让你在测试结束时留下相同的登录页面。为了调试你的应用程序或编写部分测试,你将总是需要注释掉你的自定义 cy.logout()
命令。
全是缺点,没有优点
暂时假设,出于某种原因,你的应用程序非常需要最后一点 after
或 afterEach
代码运行。让我们假设如果不运行该代码 - 一切都将丢失。
这没问题 - 但即使是这样,它也不应该放在 after
或 afterEach
钩子中。为什么?到目前为止我们一直在谈论注销,但让我们使用一个不同的例子。让我们使用需要重置数据库的模式。
想法是这样的:
在每个测试之后,我想确保数据库重置回 0 条记录,以便下一个测试运行时,它是在干净的状态下运行的。
考虑到这一点,你写如下内容:
afterEach(() => {
cy.resetDb()
})
问题是:无法保证这段代码会运行。
如果,假设你编写这个命令是因为它必须在下一个测试之前运行,那么绝对最糟糕的地方就是放在 after
或 afterEach
钩子中。
为什么?因为如果你在测试中间刷新 Cypress - 你将在数据库中建立部分状态,而你的自定义 cy.resetDb()
函数永远不会被调用。
如果这种状态清理真正需要,那么下一个测试将立即失败。为什么?因为在刷新 Cypress 时重置状态从未发生过。
状态重置应在每个测试之前进行
这里最简单的解决方案是将你的重置代码移到测试运行之前。
放在 before
或 beforeEach
钩子中的代码将始终在测试之前运行 - 即使你在现有测试中间刷新 Cypress!
这也是使用 mocha 中的根级别钩子 的绝佳机会。
放置此配置的理想位置是在 supportFile中, 因为它会在任何测试文件执行前加载。
你添加到根的钩子将始终在所有套件上运行!
// cypress/support/e2e.js 或 cypress/support/component.js
beforeEach(() => {
// 现在这在每个测试之前运行
// 在所有文件中无论什么情况
cy.resetDb()
})
重置状态有必要吗?
最后你应该问自己的一个问题是 - 重置状态甚至有必要吗?记住,Cypress 已经通过在每个测试之前清除状态自动强制执行测试隔离。确保你不是在尝试清理已经被 Cypress 自动清理的状态。
如果你尝试清理的状态位于服务器上 - 当然,清理这些状态。你将需要运行这些类型的例程!但如果状态与当前测试的应用程序相关 - 你可能甚至不需要清除它。
你唯一需要清理状态的时候,是一个测试运行的操作影响了下游的另一个测试。只有在这些情况下你才需要状态清理。
真实示例
真实世界应用(RWA) 通过一个名为 db:seed
的自定义 Cypress 任务 在 beforeEach
钩子中重置和重新种子数据库。这允许每个测试从干净的初始状态和确定性状态开始。例如:
// cypress/tests/ui/auth.cy.ts
beforeEach(function () {
cy.task('db:seed')
// ...
})
db:seed
任务在项目的 setupNodeEvents 函数中定义,在这种情况下,向应用程序的专用后端 API 发送请求以适当地重新种子数据库。
- cypress.config.js 文件
- cypress.config.ts 文件
const { defineConfig } = require('cypress')
module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
async 'db:seed'() {
// 发送请求到后端API以使用测试数据重新种子数据库
const { data } = await axios.post(`${testDataApiEndpoint}/seed`)
return data
},
//...
})
},
},
})
import { defineConfig } from 'cypress'
export default defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
async 'db:seed'() {
// 发送请求到后端API以使用测试数据重新种子数据库
const { data } = await axios.post(`${testDataApiEndpoint}/seed`)
return data
},
//...
})
},
},
})
上述相同的实践可以用于任何类型的数据库(PostgreSQL、MongoDB 等)。在这个例子中,发送了一个请求到后端 API,但你也可以直接与你的数据库交互,使用直接查询、自定义库等。如果你已经有非 JavaScript 的方法来处理或与你的数据库交互,你可以使用 cy.exec
,而不是 cy.task
,来执行任何系统命令或脚本。
不必要的等待
反模式: 使用 cy.wait(Number)
等待任意时间段。
最佳实践: 使用路由别名或断言来保护 Cypress,直到满足明确的条件才继续。
在 Cypress 中,你几乎不需要使 用 cy.wait()
来等待任意时间。如果你发现自己这样做,很可能有更简单的方法。
让我们想象以下示例:
不必要的 cy.request()
等待
在这里等待是不必要的,因为 cy.request()
命令在从服务器接收到响应之前不会解析。在这里添加等待只会在 cy.request()
已经解析后增加 5 秒。
cy.request('http://localhost:8080/db/seed')
cy.wait(5000) // <--- 这是不必要的
不必要的 cy.visit()
等待 仅限端到端测试
等待这是不必要的,因为 cy.visit() 在页面触发其 load
事件时解析。到那时,你的所有资产都已加载,包括 javascript、样式表和 html。
cy.visit('http://localhost/8080')
cy.wait(5000) // <--- 这是不必要的
不必要的 cy.get()
等待
等待下面的 cy.get()
是不必要的,因为 cy.get()
会自动重试,直到表的 tr
长度为 2。
每当命令有断言时,它们不会解析,直到其关联的断言通过。这使你能够描述应用程序的状态,而不必担心它何时到达那里。
cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }])
cy.get('#fetch').click()
cy.wait(4000) // <--- 这是不必要的
cy.get('table tr').should('have.length', 2)
这个问题的一个更好的解决方案是通过等待显式的别名路由。
cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]).as(
'getUsers'
)
cy.get('[data-testid="fetch-users"]').click()
cy.wait('@getUsers') // <--- 显式等待此路由完成
cy.get('table tr').should('have.length', 2)
智能运行测试
随着你的测试套件增长并运行时间更长,你可能会在 CI 系统上遇到性能瓶颈。我们建议将你的源代码控制系统与测试套件集成,以便在所有 Cypress 测试通过之前阻止合并。这样做的缺点是较长的测试执行时间会减慢分支合并和功能交付的速度。如果你有依赖的分支链等待合并,这个问题会进一步加剧。
这个问题的解决方案之一是使用 Cypress Cloud 的智能编排。结合并行化、负载均衡、自动取消和规范优先级,智能编排最大化你可用的计算资源并最小化浪费。
Web 服务器
最佳实践: 在运行 Cypress 之前启动 web 服务器。
我们不建议尝试从 Cypress 内部启动你的后端 web 服务器。
由 cy.exec() 或 cy.task() 运行的任何命令最终都必须退出。否则,Cypress 将无法继续运行任何其他命令。
尝试从 cy.exec() 或 cy.task() 启动 web 服务器会导致各种问题,因为:
- 你必须将进程放在后台
- 你失去了通过终端访问它的权限
- 你无法访问它的
stdout
或日志 - 每次测试运行时,你必须解决已经运行的 web 服务器的复杂性
- 你可能会遇到不断的端口冲突
为什么我不能在 after
钩子中关闭进程?
因为无法保证 after
中的代码会始终运行。
在 Cypress 测试运行器中工作时,你总是可以在测试中间重新启动 / 刷新。当这种情况发生时,after
中的代码不会执行。
那我应该怎么做?
在运行 Cypress 之前启动你的 web 服务器,并在完成后杀死它。
你是在尝试在 CI 中运行吗?
设置全局 baseUrl
反模式: 使用 cy.visit() 而不设置 baseUrl
。
最佳实践: 在你的 Cypress 配置 中设置 baseUrl
。
通过在 配置中添加 baseUrl,Cypress 将尝试为提供给 cy.visit() 和 cy.request() 等命令的任何非完全限定域名 (FQDN) URL 添加前缀。
这允许你省略在命令中硬编码完全限定域名 (FQDN) URL。例如,
cy.visit('http://localhost:8080/index.html')
可以缩短为
cy.visit('index.html')
这不仅创建了可以轻松在域之间切换的测试,即在 http://localhost:8080
上运行的开发服务器与部署的生产服务器域之间切换,而且添加 baseUrl
还可以在 Cypress 测试的初始启动时节省一些时间。
当你开始运行测试时,Cypress 不知道你计划测试的应用程序的 URL。因此,Cypress 最初在 https://localhost
+ 随机端口上打开。
未设置 baseUrl
时,Cypress 在 localhost
+ 随机端口加载主窗口

一旦遇到 cy.visit(),Cypress 就会将主窗口的 URL 切换到你在访问中指定的 URL。这可能导致测试首次启动时的“闪烁”或“重新加载”。
通过设置 baseUrl
,你可以完全避免这种重新加载。Cypress 将在测试开始时立即加载你在 baseUrl
中指定的主窗口。
Cypress 配置文件
- cypress.config.js 文件
- cypress.config.ts 文件
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:8484',
},
})
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8484',
},
})
设置 baseUrl
后,Cypress 在 baseUrl
加载主窗口

设置 baseUrl
还给你一个额外的好处,即在 cypress open
时如果服务器未在指定的 baseUrl
上运行,你会看到一个错误。

我们还会在 cypress run
时几次重试后显示错误,如果服务器未在指定的 baseUrl
上运行。

深入使用 baseUrl
这个短视频深入解释了如何正确使用 baseUrl
。