【文档翻译】Cypress - Best Practices - 最佳实践

官方文档
目录:
一、组织测试、登录、控制状态
二、 选择元素
三、分配返回值
四、访问外部站点
五、存在依赖于前一个测试的测试
六、使用单个断言来创建“小”的测试
七、使用after或afterEach钩子
八、非必要的等待
九、Web服务器
十、设置一个全局的baseUrl

Real World Practices
Cypress团队维护了Real World App (RWA)项目,这是一个在实践和可行情况下使用Cypress用于示范最佳实践和可扩展策略的全栈示例应用。
RWA 通过跨多种浏览器和设备大小的端到端测试 实现了完整的代码覆盖,并且还包括视觉回归测试、API 测试、单元测试,并在高效的 CI 管道中运行它们 。
这个应用已经打包了你需要的所有东西,只需要克隆下来就能开始测试。

一、组织测试、登录、控制状态

反面模式(anti-pattern):共享页面对象,使用自己的UI来登录,而非走捷径。
最佳实践:独立地测试页面,编程式地登录进应用,控制应用的状态。

Logging in recipes

二、选择元素

反面模式:使用“脆弱”的元素选择器
最佳实践:使用data-*属性为选择器提供上下文,并将它们与CSS或JS的更改隔离

为了省去麻烦,应该编写能够适应变化的选择器。

我们经常看到用户在定位他们的元素时遇到问题,因为:
1、应用程序使用可能会动态更改的类或 ID
2、选择器在开发更改( CSS 样式或 JS 行为)中脱离出来
幸运的是,以下方法可以避免这两个问题。
1、不要使用基于CSS属性定位,如:id,class,tag
2、不要定位那些可能会更改自己文本值(textContent)的元素
3、添加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语义耦合
cy.contains(‘Submit’).click()较推荐依然和那些可能改变的文本相结合
cy.get(’[data-cy=submit]’).click()推荐和所有可能更改的元素隔离

通过tag、class、id定位元素是非常不稳定并且容易变化的。在开发中可能替换元素、重构CSS、更新id,或者添加或删除影响元素样式的类。
相反,将data-cy属性添加到元素为我们提供了一个仅用于测试的目标选择器,data-cy属性不会因为CSS样式或则JS行为变化而改变,意味着它不会与元素的行为或样式耦合。
除此之外,它具有语义化的特点,可以让每个人都清楚这个元素是被测试代码直接使用的。
另外,cypress测试中的选择器会优先使用data-cy、data-test、data-testid。

RWA示例

RWA添加了两个用于选择元素的自定义指令
getBySel返回具有与指定选择器匹配的data-test属性的元素。
getBySelLike返回具有包含 指定选择器的data-test属性的元素。

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()
如果答案是否定的:那么使用数据属性。

三、分配返回值

在Cypress中,几乎不需要使用const、let、var,如果在使用就要尽量去掉它们。
错误实例:

const a = cy.get('a')
cy.visit('https://example.cypress.io')
// nope, fails
a.first().click()

如果您已经熟悉 Cypress 命令,但发现自己在使用 const, let, 或者var您通常会尝试执行以下两种操作之一:
1、保存并比较text、classes、 attributes等值。
2、在测试和钩子之间共享值,例如before和 beforeEach。
要使用这些模式中的任何一种,请阅读 变量和别名指南

四、访问外部网站

反向模式:尝试访问或与不受您控制的站点或服务器交互
最佳实践:只测试能够控制的内容,尽量避免与第三方交互。必要时,使用cy.request(),通过其API与第三方服务器通>信。

在以下几种情况下,您可能希望访问第三方服务器:
1、当您的应用程序通过 OAuth 使用另一个提供程序时测试登录。
2、验证您的服务器会更新第三方服务器的内容。
3、检查您的电子邮件以查看您的服务器是否发送了“忘记密码”电子邮件。
最初,您可能会想使用cy.visit()或使用 Cypress 来浏览(traverse)第三方登录窗口。
但是,在测试时永远不要使用您的 UI 或访问第三方站点,因为:
1、这非常耗时,并且会减慢您的测试速度。
2、第三方网站可能已更改或更新其内容。
3、第三方站点可能存在您无法控制的问题。
4、第三方站点可能会通过脚本检测到您正在测试并阻止您。
5、第三方网站可能正在运行 A/B 活动。
让我们来看看处理这些情况的一些策略。

登录时

许多 OAuth 提供商运行 A/B 实验,这意味着他们的登录屏幕是动态变化的。这使得自动化测试变得困难。
许多 OAuth 提供商还会限制您可以向他们发出的 Web 请求数量。例如,如果您尝试测试 Google,Google 会自动 检测到您不是人类,并且不会为您提供 OAuth 登录屏幕,而是让您填写验证码。
此外,通过 OAuth 提供程序进行的测试是可变的 - 您首先需要在他们的服务上有一个真实的用户,然后修改该用户的任何内容可能会影响下游的其他测试。
以下是缓解这些问题的潜在解决方案:
1、使用stub越过(bypass)OAuth提供商。您可以欺骗您的应用程序相信 OAuth 提供商已将其令牌传递给您的应用程序。
2、如果您必须获得真正的令牌,您可以使用cy.request()和 OAuth 提供商提供的编程API。这些 API 不会频繁地更改,并且您可以避免诸如节流和 A/B 活动之类的问题。
3、除了让您的测试代码绕过 OAuth,您还可以向您的服务器寻求帮助。有时候OAuth 令牌所做的只是在您的数据库中生成一个用户。通常 OAuth 仅在最初有用,并且您的服务器与客户端建立自己的会话。如果是这种情况,请使用 cy.request()直接从您的服务器获取会话并完全绕过提供程序。
例子

第三方服务器

有时,您在应用程序中采取的操作可能会影响另一个第三方应用程序。想象一下您的应用程序与 GitHub 集成,通过使用您的应用程序,您可以更改 GitHub 内的数据。
在运行您的测试后,使用cy.request()编程式地与GitHub的API交互,而不是使用cy.visit()访问GitHub
这样将可以避免直接接触其他应用程序的UI。

验证发送的email

通常,在处理用户注册或忘记密码等情况时,您的服务器会安排发送电子邮件。
1、如果您的应用程序在本地运行并直接通过 SMTP 服务器发送电子邮件,您可以使用在 Cypress Test Runner 中运行的临时本地测试 SMTP 服务器。 有关详细信息,请阅读博客文章 "Testing HTML Emails using Cypress”
2、如果您的应用程序使用第三方电子邮件服务,或者您无法存根(stub) SMTP 请求,则可以使用具有 API 访问权限的测试电子邮件收件箱。 有关详细信息,请阅读博客文章 “Full Testing of HTML Emails using SendGrid and Ethereal Accounts”
Cypress甚至可以在其浏览器中加载收到的 HTML 电子邮件,以验证电子邮件的功能和视觉风格
3、在其他情况下,您应该尝试使用cy.request() 来查询服务器上的端点,该端点告诉您哪些电子邮件已排队或已发送。这将为您提供一种无需 UI 即可知道的编程方式。前提是您的服务器必须暴露此端点。
4、您还可以使用cy.request()向暴露了API的第三方电子邮件收件人服务器发送请求来读取电子邮件。然后您将需要正确身份验证凭据(您的服务器或环境变量可以提供)。一些电子邮件服务目前已经提供了 Cypress 插件来访问电子邮件。

五、测试依赖于先前测试的状态

反向模式:将多个测试耦合在一起。
最佳实践:测试应该始终能够彼此独立运行 并且仍然通过 。

您只需要做一件事就可以知道您是否错误地耦合了测试,或者一个测试是否依赖于前一个测试的状态。
在测试中更改it为it.only并刷新浏览器。

如果不是这种情况,那么您应该重构并更改您的方法。
如何解决这个问题:
将先前测试中的重复代码移动到before或beforeEach钩子。
将多个测试合并为一个更大的测试。
耦合的测试:

describe('my form', () => {
  it('visits the form', () => {
    cy.visit('/users/new')
  })

  it('requires first name', () => {
    cy.get('#first').type('Johnny')
  })

  it('requires last name', () => {
    cy.get('#last').type('Appleseed')
  })

  it('can submit a valid form', () => {
    cy.get('form').submit()
  })
})

解决方案:
1、合二为一

// a bit better
describe('my form', () => {
  it('can submit a valid form', () => {
    cy.visit('/users/new')

    cy.log('filling out first name') // if you really need this
    cy.get('#first').type('Johnny')

    cy.log('filling out last name') // if you really need this
    cy.get('#last').type('Appleseed')

    cy.log('submitting form') // if you really need this
    cy.get('form').submit()
  })
})

2、每次测试前共享代码

describe('my form', () => {
  beforeEach(() => {
    cy.visit('/users/new')
    cy.get('#first').type('Johnny')
    cy.get('#last').type('Appleseed')
  })

  it('displays form validation', () => {
    cy.get('#first').clear() // clear out first name
    cy.get('form').submit()
    cy.get('#errors').should('contain', 'First name is required')
  })

  it('can submit a valid form', () => {
    cy.get('form').submit()
  })
})

六、使用单个断言创建小测试

反向模式:表现得像在编写单元测试。
最佳实践: 添加多个断言

错误实例:

describe('my form', () => {
  before(() => {
    cy.visit('/users/new')
    cy.get('#first').type('johnny')
  })

  it('has validation attr', () => {
    cy.get('#first').should('have.attr', 'data-validation', 'required')
  })

  it('has active class', () => {
    cy.get('#first').should('have.class', 'active')
  })

  it('has formatted first name', () => {
    cy.get('#first').should('have.value', 'Johnny') // capitalized first letter
  })
})

虽然从技术上讲这可以良好运行- 但这确实过“重”,而且性能不佳。

为什么你在单元测试中使用这种模式:
1、当断言失败时,您依靠测试的标题来了解失败的原因
2、你被告知添加多个断言是不好的,并接受了这一点
3、拆分多个测试没有性能损失,因为它们运行得非常快
为什么你不应该在 Cypress 中这样做:
1、编写集成测试与单元测试不同
2、您将始终知道(并且可以直观地看到)哪个断言在大型测试中失败了
3、Cypress运行一系列异步生命周期事件,在测试之间重置状态
4、重置测试比添加更多断言慢得多

Cypress 中的测试通常会发出 30 多个命令。因为几乎每个命令都有一个默认断言(因此可能会失败),即使通过限制断言,您也不会为自己节省任何东西,因为任何单个命令都可能隐式失败。
正确实例:

describe('my form', () => {
  before(() => {
    cy.visit('/users/new')
  })
  it('validates and formats first name', () => {
    cy.get('#first')
      .type('johnny')
      .should('have.attr', 'data-validation', 'required')
      .and('have.class', 'active')
      .and('have.value', 'Johnny')
  })
})

七、使用after和afterEach钩子

反模式: 使用 after 或 afterEach 钩子来清理状态。
最佳实践:在测试运行之前清理状态 。

错误实例:

describe('logged in user', () => {
  beforeEach(() => {
    cy.login()
  })

  afterEach(() => {
    cy.logout()
  })
  it('tests', ...)
  it('more', ...)
  it('things', ...)
})

Dangling state is your friend:(这里不知道怎么翻译)

Cypress最棒的部分就是它强调测试是可调试的。与其他测试工具不同,当您的测试结束时,您会在测试完成的确切时间点留下您的工作应用程序。
当测试完成时是使用您的应用的最佳机会,这使您能够比那些部分测试来逐步驱动您的应用程序,同时编写您的测试和应用程序代码。
Cypress在测试结束时并没有清除自己的内部状态,即Cypress希望在测试结束时处于悬空状态。像stubs、spies甚至routes在测试结束时都不会被删除,这意味着在应用程序运行Cypress命令时或在测试结束后手动使用程序时它们的行为将相同。
如果您在每次测试后删除应用程序的状态,那么Cypress就失去了在此模式下使用应用的能力。例如,最后的logout总会让您在最后测试结束时使用登录页面,因此不得不总是注释掉自定义的logout命令。

It’s all downside with no upside:

出于于某种原因,您的应用迫切需要最后运行after或者afterEach钩子。虽然这不错,但即使是这样也不应当使用after或者afterEach钩子。假设需要使用重置数据库的模式:

每次测试后,我想确保将数据库重置回 0 条记录,以便在下一次测试运行时,它以干净的状态运行。

出于这种考虑写了这段代码

afterEach(() => {
  cy.resetDb()
})

问题是:无法保证这段代码会运行
如果假设您编写此命令是因为它必须在下一个测试之前运行,那么将它放在after或者afterEach钩子中是非常不好的。因为如果在如果在测试过程中刷新Cypress,这个afterEach将不会被调用,在刷新后的测试将会出错,因为刷新并不会重置Cypress的状态。

应当在每次测试之前重置状态

对于上面的问题,最好的解决方式就是将重置代码移动到before或者beforeEach钩子中,这两个钩子中的代码始终在测试之前运行,即使中间刷新了Cypress。这也是在mocha中使用root level hooks的好机会。将这些放在cypress/support/index.js文件中的最佳位置。

是否需要重置状态

您应该问自己的最后一个问题是 - 是否有必要重置状态。Cypress在每次测试之前已经自动清除 localStorage、 cookies、 session 等。确保您没有尝试清理已被Cypress自动清理的状态。
如果需要清理的状态存在服务器上,无论如何都需要清理它们。但如果这个状态和当前正在测试的应用程序相关,甚至可能不需要清除。
唯一需要清理状态的情况是,当一个测试运行的操作影响到了下游的另一个测试,只有在这个情况下,才需要状态清理。

八、不必要的等待

反向模式:使用cy.wait(Number)
最佳实践:使用路由别名或断言来让Cypress在满足明确条件前不会被继续进行

在Cypress中,几乎不需要使用cy.wait(),如果发现自己正在这样做,那么可能会有一个更简单的方法。

不必要的cy.wait()

cy.request('http://localhost:8080/db/seed')
cy.wait(5000) // <--- this is unnecessary

不必要的cy.get()

等待cy.get()下面的内容是不必要的,因为会 cy.get()自动重试,直到表的tr长度为 2。

cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }])
cy.get('#fetch').click()
cy.wait(4000) // <--- this is unnecessary
cy.get('table tr').should('have.length', 2)

或者,此问题的更好解决方案是显式等待一个路由的别名。

cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]).as(
  'getUsers'
)
cy.get('#fetch').click()
cy.wait('@getUsers') // <--- wait explicitly for this route to finish
cy.get('table tr').should('have.length', 2)

九、网络服务器

反向模式:尝试从Cypress脚本中使用cy.exec()或cy.task()启动Web服务器
最佳实践:在运行Cypress之前就启动服务器

我们不建议尝试从Cypress内部启动您的Web服务器。
由cy.exec()或 cy.task()运行的任何命令最终都必须退出。否则,赛普拉斯将不会继续运行任何其他命令。
尝试从cy.exec()或 cy.task()启动 Web 服务器会导致各种问题,因为:
1、你必须后台处理
2、您无法通过终端访问它
3、您无权访问其stdout或日志
4、每次运行测试时,您都必须解决启动已经运行的 Web 服务器的复杂性。
5、您可能会遇到不断的端口冲突
为什么不能在after钩子中关闭进程?
因为不能保证在after钩子中的代码after将始终运行。
在 Cypress Test Runner 中工作时,您始终可以在测试过程中重新启动/刷新。发生这种情况时,after钩子中的代码将不会执行。
那该怎么办?
在运行 Cypress 之前启动您的 Web 服务器并在它完成后将其杀死。

十、设置全局的baseUrl

反向模式:使用cy.visit()而不设置baseUrl
最佳实践:在配置文件cypress.json设置一个baseUrl

配置文件

{
  "baseUrl": "http://localhost:8484"
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值