前言

几年前我认识“UI自动化”的经历像一次失败的网购。比如你看上一双心爱的鞋子,买来发现有些硌脚又舍不得退掉,反复安慰自己多穿几次后终于受不了把鞋扔到了角落吃灰。当初放弃UI自动化,主要是因为产品是强用户向的,迭代更新快,随着UI自动化用例越写越多,耗费的维护精力也越来越多,性价比非常低。

最近项目组面临越来越多的toB web系统的测试需求,我想起了角落里的UI自动化。商业系统相比强用户向的产品,可能更适合进行UI自动化测试:1)前端页面改动不频繁;2)以专业领域的选项设置和查询为主,用户操作路线更复杂。

相比复杂的Selenium,Cypress是一个开箱即用的e2e前端测试框架。本文将主要基于Cypress的官方文档的内容介绍如何开始使用Cypress进行自动化测试,同时给出对UI测试在质量体系中如何发挥作用的重新思考。

了解Cypress

Cypress官网称Cypress快速、简单、可靠,它面向所有使用JavaScript框架的开发和QA,支持测试一切可以在浏览器上运行的内容,是一套开源且完整的e2e下一代前端测试工具。

e2e指end-to-end,端到端的测试,这里稍作说明。“e2e测试”听起来和“UI测试”差不多,其实它们有重合也有细微的差别。UI测试的重点是保障各种场景下UI组件正确且交互符合预期,而e2e测试的重点是以用户视角(端到端)检查整个系统的功能逻辑是否正确。其实UI测试在实践过程中早就被发现不能仅仅局限在检查“该出现的按钮、文字是否都出现了”这种事情了,以Selenium为代表的UI测试已经从“关心每个页面元素”向着“模拟真实用户场景”发展了,没必要特别进行概念区分。下文出现的UI测试或e2e测试均指代广义上的“在前端/UI层面执行的测试”。

与Selenium使用webdriver与浏览器之间交互达成控制浏览器行为的方式不同,Cypress直接编写JavaScript代码在nodeJS环境中运行。

下图取自Cypress官网,说明它的使用相比传统的测试方式非常极其特别的简单,那我们开始吧~
image.png

Cypress快速开始

31740846-7bf607f0-b420-11e7-855f-41c996040d31.gif

1. 安装Cypress

以下以npm安装方式为例,其他安装方式请参考这里
系统要求

  • macOS 10.9 and above (64-bit only)
  • Linux Ubuntu 12.04 and above, Fedora 21 and Debian 8 (64-bit only)
  • Windows 7 and above
  • Node.js 8 and above

注意,对Linux系统还需要安装额外依赖,包括安装Xvfb以支持headless模式(无界面)的浏览器运行,详见这里
以macOS为例,安装步骤如下:

  1. cd /your/project/path
  2. # 当前目录安装cypress
  3. npm install cypress --save-dev

完成安装后,项目目录下出现一个cypress目录,默认情况下包含以下前4个目录:

  • fixtures:默认存放cy.fixture()方法使用的json文件,用于拦截网络请求后模拟返回
  • integration:默认存放测试js脚本,cypress运行时不指定文件则执行integration下的所有脚本
  • plugins:默认存放插件信息的目录,其中cypress/plugins/index.js默认在每个测试脚本前执行,以加载公共插件
  • support:默认存放公共信息,其中cypress/support/index.js默认在每个测试脚本执行前执行,以执行公共方法
  • results:默认存放测试结果
  • videos:默认存放用例执行的录屏,一个js文件对应一个mp4文件
  • screenshots:默认存放失败用例的截图

2. 本地启动Cypress

使用cypress open可以快速启动cypress的GUI界面。

  1. cd /your/project/path
  2. node_modules/.bin/cypress open

3. 编写用例

Cypress基于Mocha的语法组织用例,基于Chai语法进行验证,详见这里。cypress/integration/example中是一些官方案例,可参考编写。编写用例时,我们可以通过Cypress的执行界面可视化地定位元素,另外不需要因为担心检查的元素没有出现而手动添加sleep或等待逻辑,cypress会自动等待页面加载、自动重试查询元素失败的情况。

下面以有道翻译的官网为例给出一个最简单的测试用例。

  1. // touch {your_project}/cypress/integration/sample.js
  2. //用例集描述
  3. describe('sample suite', function() {
  4. // 定义用例
  5. it('first test case', function() {
  6. // 访问页面
  7. cy.visit('https://fanyi.youdao.com/')
  8. //查询DOM元素进行交互操作
  9. cy.get('.i-know').click()
  10. cy.get('#inputOriginal').type('Cypress')
  11. //查询DOM元素进行验证
  12. cy.get('#transTarget').contains('柏树')
  13. })
  14. })

4. 本地执行用例

在Cypress界面上选择浏览器,点击刚刚新建的sample.js,用例立即自动执行,执行的同时实时展示测试成功或失败的情况。修改sample.js文件保存后,用例立即重试。用例执行过程中实时展示每一行js代码的的执行情况,回溯整个过程也可以仔细观察每个步骤的页面源码,点击Console可以查看当时的详细的上下文信息。
e2e-test.png
至此,我们已经使用Cypress完成了一次简单且完整的e2e测试。

Cypress使用进阶

Cypress灵活度很高,支持丰富的前端操作。本文仅举例几个自动化测试框架常见的关注点在Cypress中如何实践。

1. 在CI平台自动化执行

Cypress提供了命令行的运行方式cypress run,下面给出一个例子。
测试准备:

  • 确认CI slave或runner所在机器已正确安装Chrome浏览器
  • 确认CI slave或runner所在机器基础软件环境正确

执行脚本:

  1. cd /your/project/path
  2. # CYPRESS_CACHE_FOLDER为cypress缓存目录
  3. CYPRESS_CACHE_FOLDER=<cypress_cache> npm install cypress --save-dev
  4. # 命令行方式执行测试用例cypress run,输出junit形式的报告
  5. CYPRESS_CACHE_FOLDER=<cypress-cache> \
  6. node_modules/cypress/bin/cypress run \
  7. --browser chrome \
  8. --reporter junit \
  9. --reporter-options mochaFile=result.xml,toConsole=true \
  10. --spec "$path/cypress/integration/sample.js"

2. 组织用例

Cypress使用mocha中的hook概念来定义测试前准备、测试后清理的内容。

  1. before:在当前脚本的所有用例执行前执行一次
  2. after:在当前脚本的所有用例执行后执行一次
  3. beforeEach:在当前脚本的每个用例执行前执行
  4. afterEach:在当前脚本的每个用例执行后执行
    1. describe('Hooks', function() {
    2. before(function() {
    3. // runs once before all tests in the block
    4. })
    5. after(function() {
    6. // runs once after all tests in the block
    7. })
    8. beforeEach(function() {
    9. // runs before each test in the block
    10. })
    11. afterEach(function() {
    12. // runs after each test in the block
    13. })
    14. })

Cypress也支持指定部分用例执行或不执行。

  1. it.only('run only', function () {
  2. // do something
  3. })
  4. it.skip('skip run', function () {
  5. // do something
  6. })

3. 测试报告合并和优化

Cypress支持多种形式的测试报告,包括junit、mochawesome等。junit输出一个xml文件,较为简单,可结合其他插件进行报告展示,若测试在jenkins平台执行也可安装junit report插件来展示报告。由于Cypress本身基于mocha,mochawesome形式的测试报告可以方便地将多个js脚本的测试数据合并,并且展示地更加详细美观。下面给出一个mochawesome报告例子。

  1. # 在当前目录安装mochawesome报告相关的依赖
  2. npm install --save-dev mocha mochawesome mochawesome-merge mochawesome-report-generator
  3. # 执行cypress定义测试报告形式为mochawesome,输出json格式的测试结果
  4. CYPRESS_CACHE_FOLDER=/disk3/test/cypress-cache \
  5. node_modules/cypress/bin/cypress run \
  6. --browser chrome \
  7. --reporter mochawesome \
  8. --reporter-options reportDir=cypress/results,overwrite=false,html=false,json=true \
  9. --spec "cypress/integration/*.js"
  10. # mochawesome-merge 整合多个json文件
  11. node_modules/.bin/mochawesome-merge "cypress/results/mochawesome_*.json" > cypress/results/mochawesome-merged.json
  12. # mochawesome-report-generator 根据json文件生成html形式的mochawesome报告
  13. node_modules/.bin/marge cypress/results/mochawesome-merged.json -o cypress/results

mocha.png

除了文中提到的,Cypress还有很多进阶功能,如Dashboard、CI集成、代码覆盖率等,因为目前还没有一一实践到就不展开说了。

Cypress的优点和局限

参考官网的自述和具体实践,Cypress的优点如下:

  1. 开源:积极的社区讨论、细致的官网文档
  2. 简单:快速安装、快速编写、可视化实时执行
  3. 时间旅行:分步信息详细记录和回溯、自动截图和录像
  4. 自动等待、自动重试:无需手动编写sleep等待元素加载,自动重试元素查询和验证
  5. 网络请求拦截:支持拦截网络请求进行参数修改和mock返回

同时,Cypress的官网也在trade-offs一节也大方给出了自己的局限之处,并且给出理由说明为什么这样设计,包括:

  1. 目前仅支持几种浏览器类型,包括Chrome、Electron、Firefox
  2. 不支持多个tab切换
  3. 不支持同时打开多个浏览器
  4. 同一用例必须访问同源域名
  5. 其他正在解决的问题:文件上传下载、iframe支持等

Cypress无法像Selenium一样支持多浏览器的并行测试,注定了它不适合进行web兼容测试。它擅长以快速、灵活、可回溯可调式的方式进行浏览器类型无关的前端逻辑验证。Cypress的github代码在持续更新,各种网站上也能看到如Cypress调用Python脚本进行数据库读写的神操作,相信它在未来还有更多可能性,其生态也会更加完整。

最后冷静一下,Cypress再厉害也不过是一个“高成本“的前端测试工具的新玩意儿,e2e测试在整个项目产品的质量保障中能起到多大作用呢?

分层测试和e2e

下图是经典的“测试金字塔”,以此为基础有了“分层测试”的理念。金字塔由下至上分别是:单元测试、服务层测试、UI测试。越往金字塔上层走:1)测试集成度越高;2)越贴近用户真实场景;3)测试执行的速度越慢;4)耗费的人力物力越多。因此,越往金字塔上层,测试占比应该越少,Google公司甚至给出了70/20/10的参考比例。e2e测试作为前端环境执行的测试,很大可能处于在UI层,或在独立前端服务中处于service层的偏上部分。看起来e2e类型的测试占比应足够少才合理,但我认为e2e测试的编写范围可以参考的标准是“足够覆盖核心场景”。
image.png

目前软件系统以前后端分离的架构来组织开发已经是趋势,不必拘泥于粗糙的分层和分层比例。对整个系统来说,各个后端服务的测试(如单元测试、接口测试)或许正处于金字塔偏下层的部分,这部分是便于被要求和重视的。而前端服务的测试往往自动化难度更大,前端开发人员很难(且很可能没必要)编写JS单测以保障正确性,测试人员也以手工测试为主来覆盖前端代码逻辑。这个缺口大概就是e2e测试生存之处。

e2e测试一方面可以弥补前端逻辑测试自动化的缺失,一方面可以最大限度的模拟用户真实场景,并用来发现下层基础测试覆盖的不足;但它编写和执行的难度大(需事先启动所有关联服务)、后期维护成本高也是既定事实。因此e2e测试“需要但应尽量精简”,我想它的应用场景大概是这样的:

  1. 编写内容:用来编写少量核心用户场景用例,对用例分级来说即P0用例;
  2. 执行场景:用于回归测试或集成测试,以检查整个系统的核心功能没有损坏;
  3. 调整策略:当e2e测试失败后,检查该用例是否可以用更下层的测试手段验证,补充对应的单元测试或服务层测试。

最后,e2e测试或UI测试选用什么样的框架、怎样进行才高效、是否对质量提升有帮助等问题,需要找到合适的项目、本着实事求是的精神、切实实践才能得出。希望这次Cypress或其它尚未被发现的好工具,最终能摆脱被扔进角落吃灰的命运,在各种类型的产品构建中都能发挥一份自己的光和热。

后记

我最早是这样发现Cypress的:某天我偶然刷到的一篇名为《Cypress.io: The Selenium killer》的文章,心中感慨“大人,时代变了?”,看到文末才发现这是一篇推广LogRocket的恰饭软文,呵呵。不过文章还是以Selenium为例客观地描述了目前UI自动化测试的困难和尴尬(以便于引出前端监控的重要性来推广LogRocket),催人泪下。

大概因为Cypress的官网也反复提到“testing without Selenium”、“differs from Selenium”(努力碰瓷大佬),才出现了很多将两者直接对立比较的文章。在我看来,Selenium作为UI自动化测试“老大哥”,经历版本迭代后已经拥有丰富且完整的框架体系,以及与同类框架相比最广泛的实践经验;而Cypress作为一个年轻的开源框架,它的主要卖点既没有替代Selenium的优势,也没有完全补全Selenium的缺点,谈不上取代Selenium。不过,这不妨碍Cypress仍然是一颗闪闪发光的、炫酷的新星,值得大家重新鼓起对UI自动化的热情,撸起袖子尝试一把。

参考

Cypress官网:https://www.cypress.io/
Cypress开发者博客:https://glebbahmutov.com/blog/
The Practical Test Pyramid:https://martinfowler.com/articles/practical-test-pyramid.html
Cypress.io: The Selenium killer:https://blog.logrocket.com/cypress-io-the-selenium-killer/