大纲:
- 基于龙猫 X 事件机制生成事件链列表
- 使用 Egg 和 Socket.IO 搭建自动化测试服务
- 利用 Puppeteer 进行自动化测试
- 测试用例的执行方案研讨
龙猫 X 作为服务于产品、运营、开发的页面搭建平台,通过可视化的拖拽、配置生成业务所需的各种页面。在日常的开发中,大多数情况下都是由运营直接完成需求,或是运营产品直接提交工单后由开发进行完成,测试在评审阶段的参与率不高。这就导致测试在功能页面的测试中无法全方位地检测页面的功能效果,同时也由于龙猫 X 项目测试少有参与,测试参与的多是经由龙猫 X 生成的页面的功能测试,所以测试对于龙猫 X 页面的内在逻辑也缺乏了解。因此,为测试方提供能掌握龙猫 X 页面逻辑关系的工具对提升页面的运行质量具有重要意义。
本文针对上述情况,团队开发出针对龙猫 X 的事件链自动化测试方案,由于与业务耦合度极高,可能对其他公司团队未必具有通用性,权当技术方案参考与交流。
开始前,直接抛出整个业务流程的模式图,以便对该功能有个全貌的认知。
简单阐述一下参与流程的几个服务,包括用户操作自动化测试的前端界面、获取页面数据的龙猫 X 服务、用于进行测试逻辑的服务以及连接测试服务的无头浏览器端。
主流程就是通过获取页面数据生成事件链,测试人员配置测试用例后触发对应事件,由测试服务和无头浏览器自动完成对应链条的测试需求,最后前端页面显示测试覆盖率及成功率。
基于龙猫 X 事件机制生成事件链列表
在说明事件链的生成机制前,我们需要研究一下龙猫 X 的通讯机制。
龙猫 X 的数据通讯机制由订阅发布模式机制组成,以下罗列了在实际业务中组件间通讯的流向示意图:
1 | A 组件(A1 事件)-> 发布事件(主动事件) -> 订阅事件(被动事件) -> B 组件(B1 事件) |
组件组织方式是扁平化的,事件关系具有零散化的特点。
用户触发组件 A 的 A1 事件,经过发布订阅机制触达组件 B 的 B1 事件。如果 B1 事件是类似于显示/隐藏、改变样式等操作,那么属于一个数据流向的终点。
如果 B1 事件是类似于发送请求等事件,请求响应成功即需要执行另一个回调命令,例如我们把 B2 看作请求的响应成功 fetchSuccess
事件,当请求响应成功后,自动执行后续连接的组件 C 的 C1 事件。
从上述说明中,我们可以构建出龙猫 X 组件间事件流模型 组件 A 事件 A1 -> 组件B事件B1
,组件B事件B2 => 组件C事件C1
。
由于订阅发布通讯机制的存在,发布事件(我们称为主动事件)天然地与订阅事件(我们称为被动事件)自动连接起来,但是关键的节点在于,仍以上述组件 B 的例子数名,组件 B 内存在两个定义的事件 B1 和 B2,它们通过组件内部的代码设定的回调联系起来,但是在我们的配置数据层面并未解决,所以我们需要一份清单总结所有龙猫 X 组件中存在的组件内被动和主动事件非用户触发连接的关系表,这个对于后续构成事件链是关键的一步。
- 将零散事件关系,以收集组件事件内联的关系表解决单个事件通讯间无法连结的问题,最终组织成事件链
- 通过某一事件,能自动触发后续事件(某些龙猫 X 定义为主动事件并非所有由用户触发,也能是组件内部触发)
上述图片展示了实际业务中的事件链示例,该事件链以名为「立即注册」的 Button 按钮组件触发,后续经由 Input 输入框组件获取已前置输入的手机号数据,内部数据流获取数据后触发 Input 组件内的另一个取值的主动事件,然后取值会检验输入框输入数据的有效性,根据有效性分流取值成功与取值失败的被动事件。如果取值失败,则会弹出 Message 组件提示输入框输入数据无效的相关信息;如果取值成功,则会用获取到的手机号和其他相关参数请求接口验证,响应后根据后台返回结果分流为进而请求另一个请求短信验证码的接口还是需要进行鉴定用户为真实用户的图形验证码操作。这就构成一条完整的事件链,整个事件链中除了输入框额外的数据收入外,用户只需触发一次「立即注册」的按钮即可检验后续流程。
在制定事件链形成方案后,那么摆在面前的仍有几个问题:
- 如何启动执行单个事件链?
- 如何知道一个事件链流程已经执行完毕?
- 在什么时机对执行结果进行断言测试?
经过分析现有龙猫组件的特点。事件链触发种类主要有以下几个类型:
- 浏览器默认事件:页面容器加载完毕、相关 SDK 注册成功等
- 用户交互触发:点击按钮、点击图片
- 组件钩子(被动性质/间接的主动事件):接口请求成功失败/收集值成功失败/表单项值改变时/及贷 SDK 的 Ready 事件
如果对浏览器的的事件循环机制有过了解的,不难联想到这都与 JavaScript 异步事件相关:
- 定时器 Timer
- HTML Parsing
- HTTP Request
- I/O(Mouse、Event)
- Promise。then
总结:
- JavaScript 宿主环境运行中的事件循环机制(定时器、UI Rendering、I/O、HTTP Request)
- 用于存储状态的表单高阶组件的取值事件(其实是相当于取值后,在组件内部设定校验逻辑)
根据上述生成事件链的机制,能将页面配置的事件链自动整理生成列表,后续需要通过测试服务进行自动化测试。
利用 Puppeteer 进行自动化测试
测试用的后端项目使用 Egg 的技术搭建,鉴于前后端信息交互可能会比较频繁的原因,搭配 Socket.IO 建立起前后端的通讯。
这里需要注意的是,Socket.IO 前后端选用的具体库包具有差异:
- 前端:
socket.io-client
- 后端:
egg-socket.io
1 | chat |
Egg 框架的好处是其生态提供了相关的插件,很好地与项目集成,开发起来体验很好。
具体实现细节参考 Egg - Socket.IO 教程
而另一方面,需要使用业内较为成熟的测试方法进行测试,我们经过对比研究后选择 Puppeteer 这款由 Google 出品的开源支持无头浏览器的框架。
Puppeteer 基于 Chrome DevTool Protocol(简称 CDP),而 CDP 基于 WebSocket,利用 WebSocket 实现与浏览器内核的快速通道。
Puppeteer 广泛应用于各大公司团队的实际业务场景:
- 通用
- 生成页面的屏幕截图和 PDF
- 自动化提交表单、模拟键鼠输入、自动化单元测试
- 网站性能分析:可以抓取并跟踪网站的执行时间轴,帮助分析效率问题
- 抓取网页内容,也就是我们常说的爬虫
- 业务实践
那么 Node 服务是如何与 Puppeteer 进行服务的呢?
Puppeteer 提供一种方法 page.exposeFunction
将能在 Node 服务进程调用的函数方法名挂载在 Puppeteer 创建的测试页面的全局变量 window
上。
当我们在触发事件链开端事件时,通过 Node 服务进而操作无头浏览器内的页面,并在专用于发布订阅模式事件关系传递中调用该挂载全局的函数,将相关参数结果返回到 Node 服务的相关服务中。
从上图可以看到,每当被动性质事件触发后,事件链中的被动事件会在触发时额外发送消息发送到 Node 服务,获得响应后跑过测试用例即将最终结果返回到前端。
那么同时多个 Node.js 进程会遭遇性能问题?
存在的问题:
- 启动耗时长
- 复用浏览器实例和页面实例
- Puppeteer 的优化手段 => Chromium Command Line
- 性能消耗高
- 定时重启无头浏览器实例的机制 => 定时器
无头浏览器实例与测试页面 Tab 持久化
- 通过请求 Headless Chrome 进程端口,获取无头浏览器实例的 WS URL,通过
browser.connect
重连 - Radis 缓存
- 存文件
- 存在 Egg App Config 中(相当于全局变量)
如此这般,就将整个流程都实现了闭环:
- 根据页面配置数据生成事件链(列表)
- 选择前置数据输入及测试用例
- 建立前后端通讯及测试服务和无头浏览器的通讯
- 触发用户交互事件
- 执行无头浏览器内页面对应事件
- 返回响应结果