Cypress 的指导思想和最佳实践

发表时间 2020-09-22
阅读5分钟

Cypress 作为全新的 UI 测试框架,不仅提供了便于快速开发的API 和实时观看界面,比 TestCafe 和 Puppeteer 区别的是,它提供了一套与二者不同的方法论,本文试着做一总结。

本文部分内容来自这篇文章

预置文件目录

成功安装 Cypress 后, 安装程序将自动生成4个文件夹作为脚手架,分别是:

cypress
├─ fixture
├─ integration
├─ plugins
└─ support

其中各部分的用途:

fixture 文件夹放置预制的常量,用于定义测试程序与被测程序的状态。关于 fixture 的概念理解,可以参考这篇文章:前端测试工具 TestCafe 测试代码结构

integration 文件夹放置测试规范(spec),也就是测试脚本。因为界面上的测试属于集成测试(integration test)一层,所以得名。

plugins 放置外部提供的程序

support 放置抽象和复用的函数,以简化测试脚本的编写。

这种明晰的目录结构提供了一种方法论:代码逻辑与数据分离,与可复用的代码块分离。

不必写 Page Object

一提到 UI 自动化测试,似乎 Page Object 是绕不过的开发模式。这种模式究竟是提高或者降低效率,不能一概而论,就像开车未必时时处处都比走路快一样。效果如何仍然取决于具体的项目和使用的框架。

目前看来,如果框架提供的 API 位于底层(比如 SeleniumPuppeteer,换句话说就是不提供页面行为级别的API,使用 Page Object 会提高开发和维护效率。

而提供了页面行为级别 API 的框架,(例如 TestCafeCypress),Page Object 似乎就有些多余,因为本来这种设计模式就是为了提供页面的行为描述,如果框架提供的 API 级别足够高,那么 Page Object 完全没有必要。

另外,上一节 中还提到了能讲重复动作抽象到 support 文件夹里,更提现了对 Page Object 的替代。

追求确定性,避免碎片化结果

UI 自动化最大的诟病和困扰,当属两条:维护困难和运行不稳定,这和 Selenium 有很深的渊源。

在 Selenium 时代,使用同一套测试代码,前后两次的测试结果有可能不一样,而且如果测试场景数量较多,很难有一次能全部通过。这种不可重复性,让测试活动几乎毫无价值,令人苦恼。

771.png

这种无法预料结果的测试称为 Flaky Tests,即碎片化的、易碎的测试。为了减轻碎片化现象,除了不断优化测试框架内部机制外,还需要使用者在设计测试过程时,抛弃一些旧习惯,有针对性的排除不确定性,在测试中的每一步追求确定性。

碎片化测试产生的原因大致有如下:

  • 影响结果的因素众多,但是测试脚本只掌控了其中一部分,没有全面掌握全部因素,导致测试结果失控。比如影响UI测试的因素就比普通接口测试的因素多很多,所以普通接口测试相对UI测试稳定很多,但是当接口庞大起来,各参数之间耦合度上升后,接口测试也会变得不稳定起来。
  • 等待机制不够合理。Selenium 的阻塞等待是造成失败的主要原因,这一点在 Puppeteer 的异步等待得到了改善,在 TestCafe 中更是加入了 反复的探测机制,如下图:

773.png

Cypress 也有类似的多次尝试机制,叫 Retry-ability

使用 Cypress 的最佳实践

为了提高测试结果的确定性,有如下一些指导思想和最佳实践:

代码操作胜过UI操作

比如要到达一个特定页面,用模拟鼠标点击链接,就不如直接访问 URL的方法好

// UI 操作,运行不稳定
cy.contains('跳转页面').click()

// 确定性较好
cy.visit('some/path')

因为 UI 操作牵涉到定位元素,引入了不确定性,所以方法不推荐

通过增加额外的 attribute 定位元素

样式、ID、包含文字等定位元素方法,虽然方便,但是随着产品的迭代,非常容易改变,造成测试脚本的过时和测试运行的失败。比如下面这些:

cy.get('#submitbutton').click()
cy.get('div.submit').click()
cy.contains('提交').click()

对策是引入额外的、稳定的、完全受控的属性。比如下面这个按钮:

<button id="main" class="btn btn-large" name="submission" role="button" data-cy="submit">提交</button>

这个按钮的众多属性中, idclassname提交 都可能会改变,尤其是 class ,相当不稳定,会引起定位不到这个按钮,引发碎片化。而额外添加的属性 data-cy 则可以完全受控,是定位按钮的上策。

定位代码如下:

cy.get('[data-cy=submit]').click()

将测试行为限定在页面之内

与之相对的是超出页面之外的测试,指:新开tab页面、新开浏览器窗口等。因为 Puppeteer 能完全操控浏览器,所以用 Puppeteer 可以访问到新开 tab 。 但是 Cypress 不支持 多Tab 的操作,因为其作者认为并无必要。如果要测试新开的tab,完全可以直接访问该页面,而不需要通过本页面触发打开,测试打开 tab 这一过程是无意义的。

如果实在想这么操作,比如有一个类似这样的链接:<a href="/foo" target="_blank"> 想要检查新开页面的内容,可以用如下的例子完成:

cy
  .get('a')
  .should('have.attr', 'href')
  .then(function(href){
    cy.visit(href)
    cy.title().should('include','新页面')
})

上面这段代码从 <a> 元素中提取了 href 属性,然后直接用 visit() 访问页面

减少条件判断

降低测试用例间的耦合度

理想情况下,各测试用例间应该是完全独立,不依赖任何其他用例,就可以成功运行。

有些断言并无必要

有下面两条理由,让我们写测试 spec时,不需要为每一步都加上断言:

自带默认断言

Cypress 很多 API 内置了默认断言(default assertions),例如:

cy.visit() 要求页面返回 text/html ,并且状态码是200

cy.request() 要求服务端真实存在并发回响应

cy.get()cy.contains()cy.find() 等定位元素的函数要求元素真实存在,或者稍等片刻存在(存在重试机制)

链式动作

如果一连串动作里,如果下游动作能正确执行,本身就是对上游动作的断言,(比如点击按钮首先就要能找到这个按钮)

cy
  .get('button')
  .should('exist') // 多余
  .click()
本站是个人博客。除非特别说明,所有文章均系原创,并采用 署名协议 CC-BY 授权。
欢迎转载,惟请保留原文链接:/tech/cypress-best-practise/