自动化测试是前端工程中的重要一环,与单元测试相比,单元测试侧重的是代码中每个最小单元的常规情况以及边缘条件的验证。
而自动化测试,更多地是从UI层级,或者说是用户的视角,进行页面元素的检查与验证。自动化测试环节的存在,保障了生产环境的稳定性。
本文中提到的自动化框架使用的是CucumberJS
,并且配合puppeteer
使用了headless
浏览器进行自动化流程,最后会使用puppeteer-to-istanbul
进行运行时的自动化测试的覆盖率收集。
首先需要介绍一下前端工程中使用的自动化框架CucumberJS
。
cucumber
Cucumber是基于BDD模式的可执行规范(Executable Specifications)的开源工具。行为驱动开发(Behavior-driven development)是一种敏捷开发的技术,鼓励开发者、测试以及非技术人员之间的协作。
Cucumber除了自动化验收测试以外,更重要的是能够使用统一的语言在团队的技术与非技术人员之间建起一座沟通的桥梁。
- 可执行性(Executable):您可以像执行代码(Java、Ruby…)一样运行这些规范,来验证、验收目标应用。当然,这一点是从技术人员的视角来看的;
- 规范性(Specification):从非技术人员的视角触发,相比验证本身,他们更加关心系统功能的清晰描述:系统在什么场景下能够做什么样的事情。
通过一种定义语句的方式,建立一种非开发式的语言,让团队成员能够进行维护,从而达到开发人员与验收人员的期望能够达成一致。
如下是场景用例定义的例子:
功能: 使用 ATM 固定金额方式取款
通常"取款”菜单包含了几个固定金额,使用这些固定金额取款可以避免从键盘输入提取金额,从而可以加速交易,提高取款的效率。
场景大纲: 固定金额取款
假如 我的账户中有余额"<accountBalance>"元
当 我选择固定金额取款方式取出"<withdrawAmount>"元
那么 我应该收到现金"<receivedAmount>"元
而且 我账户的余额应该是"<remainingBalance>"元
例子:
| accountBalance | withdrawAmount | receivedAmount | remainingBalance |
| 1000.00 | 100.00 | 100.00 | 900.00 |
| 500.00 | 500.00 | 500.00 | 0.00 |
Cucumber有多种语言实现的版本,并且可以集成到主流测试框架中。在这里我们使用的是JavaScript的版本——CucumberJS。
在测试用例编写的过程中,主要有以下三个步骤。由非技术人员进行如上feature文件的编写,技术人员负责实现具体语句的定义、保障可行性,最后运行测试用例,使得该文档/测试用例符合预期执行。
- 创建 feature 文件;
- 生成测试 Step Definitions;
- 运行测试用例。
English Keyword | Chinese simplified equivalent(s) |
---|---|
feature | 功能 |
background | 背景 |
scenario | 场景/剧本 |
scenarioOutline | 场景大纲/剧本大纲 |
examples | 例子 |
given | 假如/假设/假定 |
when | 当 |
then | 那么 |
and | 而且/并且/同时/ |
but | 但是 |
执行测试用例后,可以使用开源的cucumber报告工具来查看生成的报告。如下是常用的cucumber-reporter
。
macaca
如果你还没有开始尝试cucumberJS
,但对它BDD来进行自动化测试的方式蠢蠢欲动,那么可以上手macaca
试试。
macaca
是阿里开源的一款基于cucumber
的多端自动化解决方案。它提供了一系列的解决方案,包括计算机视觉(openCV等)、mock方案、页面元素检查工具、报告器、持续集成等生态建设。
macaca-reporter
会生成如下的脑图模式的报告结果,比常规的cucumber-reporter
更简洁好看。
推荐使用macaca
主要还是因为当你部署中有困难的时候应该比cucumberJS
更容易检索到中文解决方案。并且它的生态建设也已经提供出了一个完整的自动化解决方案。
Cucumber Coverage
不论是cucumber
还是macaca
,报告虽然展示上不同,但数据基本是一致的。那么我们会发现,报告中的百分比计算,一般都是失败用例数/总用例数得出的通过率。为什么没有整体的覆盖率呢?
其实cucumber
也是可以跑出覆盖率的,也就是可以跑出istanbul
报告的。
但是,现有的可以跑出来覆盖率的,实际上是使用cucumber来写ut。也就是说,你可以使用cucumber
来写单元测试,然后老老实实地把相应的单元测试覆盖率报告给跑出来。
使用cucumber进行单元测试如下:
// filename: rocket-launching.steps.js
const { Given, When, Then, And, But, Fusion } = require( 'jest-cucumber-fusion' )
const { Rocket } = require( '../../src/rocket' )
let rocket
Given( 'I am Elon Musk attempting to launch a rocket into space', () => {
rocket = new Rocket()
} )
When( 'I launch the rocket', () => {
rocket.launch()
} )
而这里我们是使用cucumber
来进行自动化测试的,前端使用cucumber是配合无头浏览器进行页面元素的查找与对应操作,得出预期与实际结果。实际上是没有运行前端代码的,那么无法得到代码覆盖率。
那么就要从无头浏览器动手了。
puppeteer
Puppeteer 是一个 Node 库,它提供了一套高阶 API ,通过 Devtools 协议控制 Chromium 或 Chrome 浏览器。Puppeteer 默认以 Headless 模式运行,但是可以通过修改配置文件运行“有头”模式。
Headless Chrome ,无头模式,浏览器的无界面形态,可以在不打开浏览器的前提下,在命令行中运行测试脚本,能够完全像真实浏览器一样完成用户所有操作,不用担心运行测试脚本时浏览器受到外界的干扰,也不需要借助任何显示设备,使自动化测试更稳定。
puppeteer具有如下功能:
- 生成页面 PDF。
- 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
- 自动提交表单,进行 UI 测试,键盘输入等。
- 创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的Chrome中执行测试。
- 捕获网站的 timeline trace,用来帮助分析性能问题。
- 测试浏览器扩展。
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
const page = await browser.newPage(); // 打开浏览器
await page.goto('https://example.com'); // 打开示例页面
await page.screenshot({path: 'screenshot.png'}); // 截图
await browser.close(); // 关闭浏览器
});
class: Coverage v0.9.0
Coverage 收集相关页面使用的 JavaScript 和 CSS 部分的信息。
使用 JavaScript 和 CSS 覆盖率来获取初始百分比的例子:
// 启用 JavaScript 和 CSS 覆盖
await Promise.all([
page.coverage.startJSCoverage(),
page.coverage.startCSSCoverage()
]);
// 导航至页面
await page.goto('https://example.com');
// 禁用 JavaScript 和 CSS 覆盖
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage()
]);
let totalBytes = 0;
let usedBytes = 0;
const coverage = [...jsCoverage, ...cssCoverage];
for (const entry of coverage) {
totalBytes += entry.text.length;
for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
}
console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`);
使用 Istanbul 输出一个覆盖率表格,见 puppeteer-to-istanbul.
Methods
- coverage.startCSSCoverage(options)v0.9.0
- coverage.startJSCoverage(options)v0.9.0
- coverage.stopCSSCoverage()v0.9.0
- coverage.stopJSCoverage()v0.9.0
使用puppeteer-to-istanbul收集运行时coverage
查阅puppeteer的API可以发现有Coverage相关的操作,可以让我们收集到运行时覆盖率。结合puppeteer-to-istanbul
写入到对应报告中。
(async () => {
const pti = require('puppeteer-to-istanbul')
const puppeteer = require('puppeteer')
const browser = await puppeteer.launch()
const page = await browser.newPage()
// Enable both JavaScript and CSS coverage
await Promise.all([
page.coverage.startJSCoverage(),
page.coverage.startCSSCoverage()
]);
// Navigate to page
await page.goto('https://www.google.com');
// Disable both JavaScript and CSS coverage
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage(),
]);
pti.write([...jsCoverage, ...cssCoverage], { includeHostname: true , storagePath: './.nyc_output' })
await browser.close()
})()
合并Coverage
但结果并不是那么完美,每一次用例执行都会覆盖结果,所以需要把所有feature的覆盖率结果都合并起来。
const pti = require('puppeteer-to-istanbul');
let featureCoverage = []; // 全局收集覆盖率
Before(async function () {
await featureScope.coverageStart();
});
After(async function () {
await featureScope.coverageStop();
featureCoverage = [...featureCoverage, ...featureScope.coverageJs, ...featureScope.coverageCss];
});
AfterAll(function() {
pti.write(featureCoverage);
});
忽略Coverage中生成的文件
到这里基本拿到我们想要的结果了,但仔细观察生成后的运行时JS文件,会发现有很多公用库的文件,这些文件并不需要我们进行覆盖,所以在生成过程中忽略掉:
let coverage = await page.coverage.stopJSCoverage();
// Exclude chai.js
coverage = coverage.filter(({url}) => !url.includes('chai'));
puppeteerCoverage.write(coverage);
总结
本文从cucumber
到puppeteer
,最后到pti
,一步步探索到了需要使用的目的方案,最后在方案中进行代码的细化,得到我们想要的结果。
UI自动化的过程中,也需要设置一个覆盖率,来验证我们测试用例的完备性。使用本文推荐的macaca
不失为一个良好的方案,可以在macaca
的基础上进行二次开发。