作者简介
Leo Li,携程高级软件工程师,负责度假 BDD-Test UI 自动化测试框架的研发、维护和迭代等工作。
如今无论大公司还是小公司都越来越重视测试质量。并且前端领域越来越繁荣,前端工程也越来越复杂,纯靠人力手工测试已经显得有些力不从心并且更容易出错。因此在项目中引入 BDD 理念进行自动化 UI 测试,让项目质量可以通过自动化工具来保障也被提上日程。本文将介绍携程度假团队是如何将其付诸实践,希望能给大家带来一些启发。
一、UI 自动化测试背景以及意义
在日常开发中,我们的程序出现 Bug 是一件非常正常的事情。Bug 本身并不可怕,可怕的是我们把 Bug 带到真正的生产环境中。为了减少 Bug 被带上生产环境的可能性,我们已经做了许多:从代码提交后 GitLab CI 自动执行单元测试并进行 Sonar 代码质量扫描,再交付测试同学人工测试,最后灰度发布上线。这一系列的流程已经很好地帮助我们降低了 Bug 被带上生产的概率了。
作为前端开发的我们来说,已经用上了诸如:TypeScript,EsLint 等现代化开发工具来提升代码的质量。这些工具或框架可以把一些问题在开发阶段暴露出来,但是这还远远不够。那么我们的前端工程是不是也可以使用自动化测试来帮助我们提升项目质量呢 ?
说到自动化测试,其实在后端领域是非常普遍的(主要是单元测试和API 测试),但是在前端领域却应用的非常少 (UI 自动化测试)。按照软件工程自底而上的概念,前端测试一般分为单元测试(Unit Testing)、集成测试(Integration Testing)和端到端测试(E2E Testing)。
从下面这张图可以看出:从下往上测试的复杂度(成本)将不断提高,另一方面测试的收益反而不断降低。从运行测试速度上来看,三种测试的运行速度是呈倒金字塔结构。即单元测试运行得最快,开发成本也是最低的。随后是服务测试,最后是 UI 自动化测试。
随着我们的业务高速迭代,技术不断革新,我们的系统也变得越来越复杂,需要高质量的代码设计以及高质量的代码实现去支撑。相信大家在实际工作中绝大多数遇到的是这样的场景:遇到比较大的项目,这些项目由于种种原因,前人留下了各种坑。历史代码质量非常糟糕,可能修改一个小点,却产生了一个影响主流程的毁灭性 Bug。
这也是为什么,很多小伙伴发现之前遗留的代码写的非常糟糕,只要能跑,便不会主动去重构它的原因。主要是担心重构后引起新的问题,同时也会加大测试的工作量。即便,你投入了大量的时间和精力进行了重构,可能未必得到比之前更好的效果,甚至可能由于业务的调整,辛苦重构的代码直接要被废弃了。遇到这种情况,不仅开发重构的成本是非常高的,而且测试人员对发布的信心也是不足的。
因此,我们需要引入 UI 自动化测试,针对系统的核心业务流程进行自动化测试用例的编写。当我们的代码进行了修改甚至重构,我们的自动化测试就会一次次的去运行,如果通过了,证明我们新修改的代码没有影响到主流程,如果失败了,那我们也可以第一时间发现问题,去修复我们的代码。
总结如下:
- UI 自动化测试在测试金字塔模型中处在顶层
- UI 自动化测试实现起来难度大成本高
- UI 自动化测试能有效增加开发与测试人员的信心
二、BDD UI 自动化测试理念
在说 BDD-UI-Testing 之前,我们先来看看 TDD、ATDD、BDD、DDD 这 4 个开发模式。
- TDD:测试驱动开发(Test-Driven Development)
- ATDD:验收测试驱动开发(Acceptance Test Driven Development)
- BDD:行为驱动开发(Behavior Driven Development)
- DDD:领域驱动开发(Domain Drive Design)
在我们日常工作中比较常见的其实是 TDD & ATDD 。即:我们在开发真正的代码前会开各种需求评审,技术评审,测试用例评审等会议。业务人员、产品经理、开发人员、测试人员会充分沟通,以确保需求被充分记录。在编写真正实现功能的代码之前会先要求测试人员提供测试用例。这种开发模式主要思想是:在正式编写需求功能的代码之前,先编写单元测试代码,再编写需求功能代码满足这些单元测试代码。
接下来我们来看看,我们日常开发项目时候的传统开发流程(W 模型):
在 W 模型中,每一份项目文档(PRD),都对应着一份测试文档(测试用例)。
那么我们再来看看 BDD 流程是怎么样的:
采用 BDD 流程进行开发,由外而内,持续地描述当前系统或模块的行为,并为之实现自动化(即步骤定义)。当产品代码部分完成后,右侧的一系列测试活动都已经自动化了。
从层次上来说,BDD 是基于 TDD 的,或者说在自动化测试中,TDD 所在的位置比较底层,是基础,而 BDD 则是它的演进版本。
BDD 核心的是,开发人员、QA、非技术人员和用户都参与到项目的开发中,彼此协作。BDD 强调从用户的需求出发,最终的系统和用户的需求一致。BDD验证代码是否真正符合用户需求,因此 BDD 是从一个较高的视角来对验证系统是否和用户需求相符。
看到这里,大家肯能会对上面的理论知识有点蒙圈。那么让我们来看下 BDD 的交互过程:
看到这里,我们可以来总结一下:
- BDD 是一种敏捷软件开发的技术
- BDD 提供了一种通用的,简单的,结构化的描述语言
- BDD 一般是黑盒测试,侧重 UI,TDD 一般是白盒测试,侧重代码
- BDD 一般采用集成测试,TDD 一般采用单元测试
- BDD 不只是自动化测试
三、我们的 BDD-UI-Testing 实践模式
上面说了这么多大家可能并没有什么实际的感觉,接下来我就直接放个 BDD-UI-Testing 测试用例。
最终我们将得到类似如下的自动化测试报告:
代码语言:javascript复制(截图中相关信息非真实数据)
看到这里相信大家一定很疑惑,这一句句的命令描述怎么就成为了自动化脚本了呢?这又是如何运行起来还能出现报告和截图的呢?
在解释这之前,我要先给大家演示一个朴素的 BDD-UI-Testing 自动化用例。
我们使用一个大家都很熟悉的 ToDoList APP 来带大家进入 BDD-UI-Testing。
BDD 测试是模拟用户行为的测试,而用户的操作又是连贯的,因此这里我们不能单纯的测试一个组件是否能正常运行,而是要测试整体。
1)用户打开 TODO App 页面
2)用户在输入框内输入 BDD-UI-Testing
3)用户按下回车
4)TODO List 显示 BDD-UI-Testing,并且输入框被清空。
那我们的 BDD 测试该如何去实现呢?请看下图:
如图所示,大家就看到了一个朴素的 BDD 测试用例,但是现在还算不上自动化。为什么呢?细心的朋友已经发现了,模拟用户的第一步,打开浏览器竟然没有,并且操作也不是在浏览器里点点点的。
目前的测试用例,我们是使用 Jest Enzyme 像爬虫一样解析页面,找到 DOM 并进行断言的。虽然用了自然语言去描述我们的测试用例了,但是还要编写 JS 代码,这还有一定的学习成本。这对我们的测试同学来说,就是阻碍他们用上自动化测试的绊脚石。
那有没有办法能直接使用自然语言编写,让我们的测试不写一行代码,进一步降低自动化学习成本,并且还能打开真正的浏览器,去模拟用户“点点点“的行为呢?
答案自然是:有的!
3.1 框架选型:Cucumber Puppeteer = @ctrip/cucumber_web_common
我们的目标是:自然语言编写,行为驱动自动化脚本。让测试一目了然,高效开发测试脚本。
因此,我们选用了 Cucumber.js 作为 BDD 测试框架,Puppeteer 来操纵浏览器模拟用户行为。
Cucumber 使用了一种叫 Gherkin 的剧本语法,支持多种自然语言来描述测试用例。
- 一种 DSL(特定领域的语言)
- 业务人员也可以读懂
- 可以用来描述软件行为
- 支持多语言
Cucumber 项目结构大致是这样的:
1)Feature 文件(剧本文件)
2)Step Definitons (步骤定义)
3)Support Code (支持代码)
4)Cucumber Command(测试套件)
Feature 文件(剧本文件)
测试项的目运行文件都在 features 目录下,以 .feature 结尾的为剧本文件,一个剧本文件中可以包含多个场景,一个场景包含多个操作步骤。
以下是一个简单的 /features/trip.feature 文件:
Step Definitons (步骤定义)
.feature 文件中描述的业务步骤要运行起来,需要根据业务场景定义操作行为。具体的业务行为是由相对应的自动化脚本来实现。
这部分自动化实现脚本(代码)主要定义在 step_definitions 目录下。
以下是一个伪代码实现的 /step_definitions/myStep.js 文件:
Support Code (支持代码)
自动化脚本在执行的过程中,比如上文中提到的 browser,作为浏览器的驱动,需要抽象出来,单独放在 support 目录下。这里还可以为统一为操作步骤定义超时时间,编写场景执行前后触发的函数等。
Cucumber Command(CLI 与 测试套件)
上面几个步骤结合起来就是一个简单的自动化测试用例。
其中步骤定义中的基础代码是 JavaScript,而自动化库使用 Puppeteer Node 库。
想要运行这个 BDD 测试用例,则需要用到 Cucumber-CLI 提供的一些命令。
- 运行匹配到的自动化用例
$ cucumber-js features/**/*.feature
- 运行某个目录下的自动化用例
$ cucumber-js features/dir
- 运行某个自动化用例
$ cucumber-js features/trip.feature
- 运行自动化用例的指定行
$ cucumber-js features/trip.feature:3
- Specify a scenario by its name matching a regular expression
$ cucumber-js --name "trip 1"
当然,这些 CLI 命令可能不够友好,大家更喜欢使用 GUI ,我们也推荐使用 CukeTest 这款测试软件,来编写测试用例,以及使用 GUI 按钮来运行测试用例。
可视化模式下的测试用例:
文本模式下的测试用例:
Feature:测试 Trip.com 搜索
打开 Trip.com , 输入目的地,点搜索,搜索结果应该包含目的地
Scenario:Trip.com 搜索
Given 浏览器导航到"trip.com"
Then 在目的地输入框内输入"上海"
Then 点击"搜索"
And 验证搜索列表页内包含"上海"
关于 Puppeteer
前面介绍了 Cucumber 这款 BDD 自动化测试工具,大家可以简单的理解为:
- Cucumber 定义了一种 DSL(领域特定语言)
- Cucumber 可以用自然语言描述测试步骤(非技术人员也能看懂测试用例)
- Cucumber 帮我们控制流程并执行相关逻辑
- Cucumber 并不负责驱动浏览器,操作浏览器的事情交给 Puppeteer
所以 Puppeteer 到底是何方神圣呢 ?
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.
用官方的话解释:Puppeteer 是一个 Node 库,它提供了高级的 API 并通过 DevTools 协议来控制 Chrome 或Chromium 。通俗来说就是一个 Headless Chrome 浏览器(也可以配置成有 UI 界面的,默认启动的是一个没有界面的)。
Puppeteer 的结构图如下所示:
简单的来说:Puppeteer 使用 DevTools 协议与浏览器进行通信并操纵他们。
这里我们直接举一个简单例子:
我们使用 Puppeteer 打开无头浏览器,访问 https://hk.trip.com/ 并截图。
效果如下图所示,仅需要 6 行代码,我们就实现了访问页面 获取性能数据 截图。
相信大家看完这个例子,对于前面的 Cucumber Feature 中写的 Given 浏览器导航到 "trip.com" 应该知道如何去实现了吧。
当然对于 Puppeteer 能做的远远不止这些,这里列举一些 Puppeteer 可以应用的场景:
- Puppeteer 可以作为高级爬虫使用
- SEO 优化(抓取 SPA 单页应用,并生成相应的预渲染内容返回)
- UI 自动化测试
- 页面性能测试与分析(捕获网站的 timeline trace 进行数据分析)
- 前端监控系统(定时访问页面,抓取相关信息,检查是否有白屏报错等)
我们是如何组合使用,并封装成框架的呢?
回到我们的原始需求上:降低自动化测试门槛
- 测试人员不用或很少去写代码
- 非技术人员也可以读懂测试用例
这就需要我们把 Cucumber 和 Puppeteer 进行封装组合使用。
- 使用 Cucumber 写的测试用例(自然语言)可以认为是 DSL
- 在代码中对该 DSL 进行解析,映射成具体 JS 代码
- Puppeteer 负责执行具体命令(如:打开浏览器、点击某按钮)
- 封装通用的步骤命令,只需要组合,使用者不需要关心具体实现
现在我们来回看下面这张图:
1)我们在 Cucumber Hooks 中定义了相关钩子函数,把打开浏览器等每个测试用例需要做的通用工作给做了;
2)在 Cucumber Word 对象上挂载了浏览器和页面的实例。这样我们就可以通过 this.browser 和 this.page 在我们的步骤定义中方便的操控 Chrome 了;
3)封装相关通用步骤,如:Given 打开"xxx"页面,Then 截图;
如何识别打开"xxx"页面 ,点击"xxx"按钮 ?
看完了上面的介绍,大家已经明白如何打开浏览器,并访问一个页面了,也能大概知道如何使用 Puppeteer 去模拟点击了。
但是我们写的打开"跟团游"页面和点击"去预定"按钮中的 "跟团游" 和 "去预定" 又是如何识别的?
其实答案非常简单:我们去编写一个配置文件去映射相关 URL 或元素。
DOM 元素选择器配置是按照页面维度来的:
如何查找元素的问题是解决了,但是不知道大家看到这里的时候有没有发现一个问题。
现在我们使用现代化前端开发框架进行开发,例如 React,因此我们可能不再需要 jQuery 时代一样在元素上加上 id="name" ,但是这就导致我们元素的 CSS 选择器 有时候又长又臭。例如:'#root > div > div > section > main > div.relative > div > button.ant-btn.search.ant-btn-primary'。
并且有时候我们的 id 会被埋点占用,并且是动态生成的,例如: '#order-10650004336-filter-order-18 > span'。并且随着版本的迭代,元素的 DOM 结构可能有所变化。
这就导致我们使用 className 或 Xpath 选择的元素并不靠谱。
可能由于一个小小的改动,导致按钮点不到,导致整个 Case 失败。
增加 test-id,保证选择器的可靠性
由于普通的 Class 选择器等并不靠谱,我们需要开发在写代码时加入稳定的 data-test-id 自定义属性作为我们自动化测试埋点,有了这些我们的自动化用例就不会因为 DOM 结构的频繁修改而导致选择不到相关元素。如下图:
修改为:使用 test-id 作为选择器后,我们也大大增加了可维护性,并把这些作为自动化测试用例“资产”的一部分。
至此为止,我们的自动化框架已经搭建完成,接下来我们进行技术总结:
- Cucumber 负责翻译自然语言(DSL)
- Cucumber 负责控制流程,执行相关逻辑
- Puppeteer 负责驱动浏览器,模拟用户的操作
- DOM 元素需要加上 test-id 以供自动化测试使用
- 提供自动化测试核心框架 @ctrip/cucumber_web_common ,发布公司 NPM 仓库
- 提供详细的文档,以供大家查阅步骤如何使用
- 提供自动化项目模板,以供大家快速搭建一个自动化测试项目
- 提供的公共步骤可以覆盖 80% 的通用场景
- 对于某些复杂的步骤则可能需要自己编写代码去实现
- 对接登录团队解决自动化测试登录出现风控的问题等
如何持续集成(DevOps)?
为什么当前我们需要DevOps,甚至很多大型的互联网公司也在进行DevOps转型,其中最关键是因为其核心思想能够满足当前业务和技术变革的需要,那就是“快速的交付价值,灵活的响应变化”。“快速的交付价值”意味着能先人一步占领市场,“灵活的响应变化”亦意味着减少变化带来的不利因素,使企业立于不败之地。
为什么要 DevOps ,以及想了解更多有关 DevOps 的实践,请查看《 携程酒店DevOps测试实践》。
回归到我们的 BDD-UI-Testing:自动化测试框架有了,测试用例也有了,那我们只是在自己的开发机上跑跑吗?
对于持续集成这块,我们接入了 Ctrip DevOps 流程,使用 GitLab CI 和 Ctrip PaaS 。基本流程如下:
开发侧流程:
- 业务代码变更,提交到 GitLab
- 触发 GitLab CI 进行代码质量扫描检测
- 触发 Ctrip PaaS CD 自动进行发布(或手动发布)
- 测试环境发布完成后,PaaS 平台的 WebHooks 通知我们发布结果,并写入消息队列中
- 消息推送到我们自动化测试代码 GitLab 仓库,触发 GitLab CI 进行 BDD-UI-Testing
- 测试数据落地,自动发送测试报告邮件,生成相关测试报告并上传测试平台
测试侧流程:
- 测试用例变更,提交到 GitLab
- 触发 GitLab CI 进行代码质量扫描检
- 触发 GitLab CI 进行 BDD-UI-Testing
- 测试数据落地,自动发送测试报告邮件,生成相关测试报告并上传测试平台
平台侧流程:
- 用户在测试平台点击运行测试用例
- 调用 API 并写入消息队列
- 消息推送到我们自动化测试代码 GitLab 仓库,触发 GitLab CI 进行 BDD-UI-Testing
- 测试数据落地,自动发送测试报告邮件,生成相关测试报告并回传至测试平台
在 GitLab CI 上使用并行模式,加快测试速度(充分榨干服务器性能)
参考 Cucumber-CLI 文档
- 我们可以使用 --parallel <NUMBER_OF_SLAVES> 来指定并行数量
- 或在 GitLab CI 环境变量中设置
CUCUMBER_PARALLEL=true 启用并行模式
CUCUMBER_TOTAL_SLAVES=10 使用 10 个进程
CUCUMBER_SLAVE_ID=0 ID for slave ('0', '1', '2', etc.)
实测:在并行 10 个进程的模式下,中型项目可以在 2分30秒内测试完成。
四、小结与展望
本文简单的介绍了携程度假团队是如何将 BDD-UI-Testing 付诸实践的。使用 Cucumber 作为 BDD 自动化测试工具,使用 Puppeteer 来操控浏览器,使用 GitLab CI 对自动化测试持续集成。
通过本文我们也了解了如何搭建一个 BDD UI 自动化测试框架并加入 DevOps 流程。希望本文能给大家带来一些启发和收获。
最后是一点小小的提醒:
- UI 自动化测试达到 70%-80% 的通过率很容易
- 但是想把通过率提升到 90%-100% 却很难
- 每上升 1% 的通过率,我们可能都要为此付出巨大的代价
- 因此我们不能盲目追求高通过率,需要思考这是否值得
目前,我们实施的 BDD-UI-Testing 还处于初期阶段,很多方面尚未完全达到预期。对于自动化测试我们还有很多的工作需要去做:
- 加入 AI 图像对比,对比修改后的代码是否对页面产生了不可预期的影响
- 需找更好的 Mock 数据方案(本地 Mock 数据 和 Mock 平台返回固定的数据都不够灵活)
五、大家关心的问题
5.1 为什么使用 Puppeteer 而不使用 Selenium ?
- Puppeteer 是由 Google 官方团队出品,技术较新前景更好,与 Chrome 有更好的兼容性
- Puppeteer 更好的支持 SPA(Single-Page Application)页面的测试
- 单一语言,我们的 BDD 框架挑选了 Cucumber.js 并且 Puppeteer 也是使用 JavaScript 编写的 Node.js 库 。因此这二者可以更好的结合,并且更加方便在浏览器中调试。
- 更简单的拦截网络请求(可以更加方便的 Mock 接口等)
5.2 我可不可以使用 Selenium ?
当然可以!甚至你可以不使用 JavaScript 来编写。Cucumber 这款 BDD 自动化测试框架支持多种编程语言,你可以挑选任意你喜欢的语言去与 Selenium 进行组合。
https://cucumber.io/docs/installation/
5.3 BDD-UI-Testing 只适用 Web 端吗 ?
并不是这样的,在 APP 端 (Native 或 CRN)我们通用可以使用同一套命令,使用 Cucumber 结合 AirTest 进行 APP 侧的 BDD 自动化测试。甚至我们可以使用 Native 提供的 Bridge 进行命令封装,达到操控真机的目的。
对于 RN 项目我们也可以使用 RN 转 RN Web 的办法,用 Cucumber Puppeteer 来测试我们业务的核心流程。