包阅导读总结
1.
关键词:JavaScript、代码覆盖率、Canyon、端到端测试、前端质量
2.
总结:本文介绍了针对 JavaScript 代码质量提升的全面覆盖率分析工具 Canyon,它解决了端到端测试覆盖率收集难题,支持代码插桩、测试与上报、覆盖率聚合和报告生成等功能,还提供了变更代码覆盖率分析和优先级列表,目前已在携程内部使用并将开源。
3.
主要内容:
– 背景
– istanbuljs 在端到端测试覆盖率检测上的不足
– 携程前端技术发展对端到端测试覆盖率收集的需求
– 介绍
– 通过 Babel 插件配置实现多种功能
– 基于 JavaScript,适用于云原生环境,与 CI/CD 工具无缝集成
– 架构图展示主要功能
– 代码覆盖率
– 原理是在代码执行前插入探针和计数器
– 需多次测试达到高覆盖率
– 代码插桩
– 编译时插桩更精确
– 不同工程类型有相应插桩方案
– 插桩成功检查及关联源代码展示
– 注意插桩对代码产物大小的影响
– 测试与上报
– 以 playwright 为例说明测试中收集和上报覆盖率数据
– 解决多页面覆盖率收集时机问题
– 提及 Chrome 新 API 对数据收集的作用
– 聚合
– 使用 reportID 关联和聚合覆盖率数据
– 针对单体数据体积大采用消息队列和无状态服务
– 报告
– 沿用 istanbul-report 风格,使用 monaco-editor 标记源代码覆盖率
– 变更代码覆盖率
– 统计公式,通过配置对比获取变更代码行结合计算
– react native 覆盖率收集方案
– 插桩方案适用,在流水线中打包,利用 websocket 暴露数据
– 覆盖率提升优先级列表
– 结合生产环境覆盖率数据创建优先级列表及覆盖率权重公式
– 社区推广
– Canyon 开源,未来有发展空间和开发计划
思维导图:
文章地址:https://mp.weixin.qq.com/s/aTNtVmCeZ94YYscu58Y4Vg
文章来源:mp.weixin.qq.com
作者:机票质量工程
发布时间:2024/6/17 0:01
语言:中文
总字数:5074字
预计阅读时间:21分钟
评分:93分
标签:JavaScript,代码质量,覆盖率分析,端到端测试,CI/CD
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
前言
Canyon,一款针对 JavaScript 代码质量提升的全面覆盖率分析工具,专为端到端测试设计,支持高效的代码插桩、测试与上报、覆盖率聚合和报告生成,以及提供变更代码覆盖率分析和优先级列表,推动前端代码质量的持续改进。今日前端早读课文章由 @机票质量工程分享,公号:携程技术授权。
@wr_zhang25,携程资深前端开发工程师,关注前端代码覆盖率、JavaScript 开源方向。
@Liang, 携程资深研发经理,质量专家,专注质量工程领域。
正文从这开始~~
一、背景
istanbuljs 是一款优秀的 JavaScript 代码覆盖率工具,主要用于单元测试的代码覆盖率检测和生成本地覆盖率报告。然而,随着现代前端技术和 UI 自动化测试的发展,对端到端测试的代码覆盖率检测需求逐渐增加,istanbuljs 提供的功能显得捉襟见肘。
在携程内部 JavaScript 代码覆盖率使用的是 gitlab 内置的 coverage 上报,也是只支持单元测试的覆盖率收集和概览数据展示。随着携程的前端技术日益精进,我们有了自己的前端流量录制平台,并且部署了相当大规模的模拟器集群进行 UI 自动化 (flybirds) 回放。这种场景下,需要对端到端测试的代码覆盖率进行收集和展示,以便开发同学更好的了解到自己的代码质量。
传统的 istanbuljs 提供的功能已经无法满足我们的需求。我们需要处理 UI 自动化过程中来自前端高并发的覆盖率上报,实时的覆盖率聚合,以及覆盖率数据的聚合展示。因此,我们在 Istanbuljs 的基础上开发了 Canyon,解决端到端测试覆盖率难收集的问题。
目前,携程的多个部门已经开始使用 Canyon,并在持续集成流水线构建阶段插入探针代码,在 UI 自动化测试阶段收集和上报覆盖率数据。服务端实时生成详尽的覆盖率报告,为 UI 自动化测试用例提供全面的覆盖率数据指标。
二、介绍
Canyon 通过简单的 Babel 插件配置即可实现代码插装、覆盖率上报和实时报告生成。其技术栈完全基于 JavaScript,只需 Node.js 环境即可运行,部署方便,适用于云原生环境的部署(如 Docker、Kubernetes)。
应用的架构设计适用于处理高频、大规模的覆盖率数据上报,能够应对 UI 自动化测试中的各种场景。同时,Canyon 与现有的 CI/CD 工具(如 GitLab CI、Jenkins)无缝集成,使用户能够轻松地在持续集成流水线中使用。
架构图如下:
下面会根据以下几个部分来介绍 Canyon 的主要功能:
-
代码覆盖率
-
代码插桩
-
测试与上报
-
覆盖率聚合
-
覆盖率报告
-
变更代码覆盖率
-
react native 覆盖率收集方案
-
覆盖率提升优先级列表
三、代码覆盖率
随着编写更多的 end-to-end 测试 case,你会发现有一些疑问,我需要写更多的测试用例吗?究竟还有哪些代码没测到?用例会不会重复了?这个时候代码覆盖率就派上用场了,它的原理是在代码执行前将代码探针插入到源代码中(其实就是上下文加计数器),这样每当 case 执行的时候就可以触发其中的计数器。
在代码中插入代码探针的步骤称为代码插桩(instrument)。插桩前的代码:
// add.js
function add(a, b) {
return a + b
}
module.exports = { add }
插桩过程是对代码解析以查找所有函数、语句和分支,然后将计数器插入代码中。对于上面的代码,插桩完成后:
// 这个对象用于计算每个函数和每个语句被执行的次数
const c = (window.__coverage__ = {
// "f" 记录每个函数被调用的次数
f: [0],
// "s" 记录每个语句被调用的次数
// 我们有3个语句,它们都从0开始
s: [0, 0, 0],
})
// 第一个语句定义了函数
c.s[0]++
function add(a, b) {
// 函数被调用后是第二个语句
c.f[0]++
c.s[1]++
return a + b
}
// 第三个语句即将被调用
c.s[2]++
module.exports = { add }
我们希望确保文件中的每个语句和函数 add.js 都已被我们的测试至少执行一次。因此我们编写一个测试:
// add.cy.js
const { add } = require('./add')
it('adds numbers', () => {
expect(add(2, 3)).to.equal(5)
})
当测试调用时add(2, 3)
,执行 “add” 函数内的计数器递增,覆盖范围对象变为:
{
f: [1],
s: [1, 1, 1]
}
这个测试用例覆盖率达到了 100%,每个函数和每个语句都至少执行了一次。但是在实际应用中,要达到 100% 的代码覆盖率需要多次测试。
这是覆盖率的基本介绍,有了这个前置知识,方便大家理解下面的内容。
四、代码插桩(instrumenting-code)
代码覆盖率最重要的一环就是代码插桩
istanbuljs 是久经沙场的 js 代码插桩黄金标准。Canyon 主要为端到端测试提供解决方案,经过大量的实验验证,现代化前端工程的覆盖率插桩必须要编译时插桩。具体原因是 istanbuljs 提供的 nyc 插桩工具只能对原生 js 进行插桩,然而前端模版语法层出不穷,例如 ts、tsx、vue,虽然 nyc 也可以插桩,但是结构实践证明直接插桩的覆盖率效果不尽人意,无法精确到该插桩到的函数、语句、分支。
幸运的是经过调研,我们发现了 babel-plugin-istanbul、vite-plugin-istanbul (experimental)、swc-plugin-coverage-instrument (experimental)。等类型工程的插桩解决方案。这些方案无一例外都是在前端工程编译阶段在将代码分析成 ast 抽象语法树的时候在适当时机进行插桩方法调用,更精确的插桩到的函数、语句、分支。
适用的工程类型:
工程类型 | 方案 |
---|---|
vanilla javascript | nyc |
babel | babel-plugin-istanbul |
vite | vite-plugin-istanbul (experimental) |
swc | swc-plugin-coverage-instrument (experimental) |
用户可以根据自己的工程类型选择合适的插桩方案,只需要在工程中安装对应的插件,然后就会在编译时自动插桩。
以 babel.config.js 为例:
module.exports = {
plugins: [
[
'babel-plugin-istanbul',
{
exclude: ['**/*.spec.js', '**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.jsx'],
},
],
],
};
插桩完成后,代码中会插入一些代码探针,这些代码探针会在运行时收集覆盖率数据,然后上报到 Canyon 服务端。
检查是否插桩成功,可以在编译后的产物中搜索__coverage__
,如果有则说明插桩成功。
为了紧密关联插桩代码的源代码,我们适配了各种 provider,将环境变量发送到 Canyon 服务端,兑换到 reportID,方便覆盖率数据聚合计算完成后的覆盖率源文件的关联展示。
我们还提供了 babel-plugin-canyon 的 babel 插件,可以在各种流水线内(aws,gitlab ci)读取环境变量 (branch、sha),以供后续覆盖率数据与对应的 gitlab 源代码关联。
babel.config.js
module.exports = {
plugins: [
[
'babel-plugin-canyon',
{
provider: 'gitlab',
branch: process.env.CI_COMMIT_REF_NAME,
sha: process.env.CI_COMMIT_SHA,
},
],
],
};
支持的提供商:
-
Azure Pipelines
-
CircleCI
-
Drone
-
Github Actions
-
GitLab CI
-
Jenkins
-
Travis CI
需要特别注意的是,代码探针的插桩会在构建产物上下文加上代码探针,会是代码整体产物增大 30%,建议不要上生产环境。
五、测试与上报
当插桩完成发布到测试环境后,我们就可以进行测试了。拿 playwright 举例,对于插桩成功的前端应用站点,window 对象上面都会挂载__coverage__
和__canyon__
对象,我们需要在 playwright 测试过程中收集并上报这些数据到 canyon 的服务端。
playwright 示例:
const {chromium} = require('playwright');
const main = async () => {
const browser = await chromium.launch()
const page = await browser.newPage();
// 进入被测页面
await page.goto('http://test.com')
// 执行测试用例
// 用例1
await page.click('button')
// 用例2
await page.fill('input', 'test')
// 用例3
await page.click('text=submit')
const coverage = await page.evaluate(`window.__coverage__`)
// 收集上报覆盖率
upload(coverage)
browser.close()
}
main()
携程内部有自己的 UI 自动化平台 flybirds,我们在 flybirds 内部集成了 Canyon 覆盖率数据的收集和上报。真实的浏览器 UI 自动化测试的覆盖率收集场景较为复杂,主要体现在多页面(MPA)的覆盖率收集时机不确定性。
单页面(SPA)与多页面(MPA)
当测试用例执行完成后,对于单页面应用 (SPA) 或者多页面应用而言,上报步骤是将页面 window 对象上的__coverage__
对象上报到 Canyon 服务端,对于单页面应用来说,相对来说比较简单,在所有测试内容都在单页面应用内,覆盖率数据会常驻在 window 对象中,对于多页面应用而言,路由的跳转会导致 window 对象的重制,丢失 coverage 对象。所以这个时机是至关重要的,经过大量实践验证,我们找到了浏览器的 onvisiblechange 方法。
在浏览器可见性改变的时候上报覆盖率数据,值得一提的是,对于 visibilitychange 这种可能会导致重复数据上报,但是对于覆盖率统计来说,未执行到的代码多次合并来说不会影响覆盖率的具体指标数据统计。
Chrome 浏览器正在积极引入一个革命性的 JavaScript API——fetchLater ()。这个全新的 API 旨在彻底简化关闭页面时的数据发送过程,确保即使在页面关闭后或用户离开的情况下,请求也能在未来某个时刻被安全、可靠地发出。
这个 API 的推出时令人振奋的,可以很好的解决多页面(MPA)收集难的问题,只需要在浏览器关闭时收集。
注:fetchLater()
已在 Chrome 中提供,用于在版本 121(2024 年 1 月发布)开始的原始试验中供真实用户测试,该试验将持续到 Chrome 126(2024 年 7 月)。
六、聚合
覆盖率数据的来源是同一版本的代码,覆盖率数据是可以聚合的,Canyon 内部使用 reportID 来关联测试用例和细分聚合维度。这样做可以让海量的覆盖率数据聚合成有限个,即 Case 的数量。
/**
* 合并两个相同文件的文件覆盖对象实例,确保执行计数正确。
*
* @method mergeFileCoverage
* @static
* @param {Object} first 给定文件的第一个文件覆盖对象
* @param {Object} second 相同文件的第二个文件覆盖对象
* @return {Object} 合并后的结果对象。请注意,输入对象不会被修改。
*/
function mergeFileCoverage(first, second) {
const ret = JSON.parse(JSON.stringify(first));
delete ret.l; // 移除派生信息
Object.keys(second.s).forEach(function (k) {
ret.s[k] += second.s[k];
});
Object.keys(second.f).forEach(function (k) {
ret.f[k] += second.f[k];
});
Object.keys(second.b).forEach(function (k) {
const retArray = ret.b[k];
const secondArray = second.b[k];
for (let i = 0; i < retArray.length; i += 1) {
retArray[i] += secondArray[i];
}
});
return ret;
}
端到端测试的覆盖率数据特点之一是单体数据体积大,在项目整体插桩的情况下相当于整体源代码体积的 30%。携程 Trip.com flight 站点的预定页 UI 自动化 case 上报次数每次可达 2000 次,每次 10M 数据,这样的数据量对于 Canyon 服务端来说是一个巨大的挑战。
对于单条数据大且高频次的数据上报场景,很难做到实时数据聚合计算。Canyon 采用消息队列的形式来消费数据,并且设计成无状态服务,适用于云原生时代的容器化部署,可通过 HPA 弹性伸缩容来应用不同场景下的测试覆盖率上报。
七、报告
对于覆盖率报告展示,我们沿用了 istanbul-report 的界面风格,但是由于 istanbul-report 只提供了静态 html 文件的生成,不适合现代化前端水合数据生成 html 的模式,为此我们参考了它的源码,使用了 monaco-editor 标记源代码覆盖率。
const decorations = useMemo(() => {
if (data) {
const annotateFunctionsList = annotateFunctions(data.coverage, data.sourcecode);
const annotateStatementsList = annotateStatements(data.coverage);
return [...annotateStatementsList, ...annotateFunctionsList].map((i) => {
return {
inlineClassName: 'content-class-found',
startLine: i.startLine,
startCol: i.startCol,
endLine: i.endLine,
endCol: i.endCol,
};
});
} else {
return [];
}
}, [data]);
经过着色后的效果:
八、变更代码覆盖率
对于变更代码覆盖率,我们统计的公式是覆盖到的新增代码行 / 所有新增代码行。
通过配置 compareTarget 来指定对比目标,再联合 gitlab 的 git diff 接口获取变更代码行结合覆盖率数据计算。
/**
* returns computed line coverage from statement coverage.
* This is a map of hits keyed by line number in the source.
*/
function getLineCoverage(statementMap:{ [key: string]: Range },s:{ [key: string]: number }) {
const statements = s;
const lineMap = Object.create(null);
Object.entries(statements).forEach(([st, count]) => {
if (!statementMap[st]) {
return;
}
const { line } = statementMap[st].start;
const prevVal = lineMap[line];
if (prevVal === undefined || prevVal < count) {
lineMap[line] = count;
}
});
return lineMap;
}
九、react native 覆盖率收集方案
携程的移动端技术栈主要是 react native,好消息是对于我们的插桩方案一样适用,因为都是基于 babel 编译。并且得力于得力于公司内部的 react native 项目结构统一,我们将编译时插桩做到了流水线中,在流水线中分别打包 “正常包” 和” 插桩包 “,这样搭配 UI 自动化可以形成一套完整的录制回放覆盖率指标收集的测试体系。
利用 websocket 暴露模拟器内覆盖率数据:
// 创建WebSocket连接
const socket = new WebSocket('ws://localhost:8080');
// 当WebSocket连接打开时触发
socket.onopen = () => {
console.log('Connected to coverage WebSocket server');
};
// 当收到WebSocket消息时触发
socket.onmessage = event => {
try {
if (JSON.parse(event.data).type === 'getcoverage') {
// 发送覆盖率数据
socket.send(JSON.stringify(payload));
}
} catch (e) {
console.log(e);
}
};
// 当WebSocket连接关闭时触发
socket.onclose = () => {
console.log('Disconnected from coverage WebSocket server');
};
目前携程机票部门的 APP 模块均已接入 Canyon,经过实践 istanbuljs 可以很好的对其进行插桩及覆盖率数据收集,测试团队在每次生产发布前会以 Canyon 的覆盖率数据指标来衡量此次发布的质量情况。
十、覆盖率提升优先级列表
在用户最初接入 Canyon 系统时,会面临一个挑战:如果没有大量的 UI 自动化测试用例,大型应用的代码覆盖率会显得尤为低下。一开始,仅仅提供一个 Istanbul 代码覆盖率报告,并不能有效指导团队如何提高覆盖率,这让大家感到困惑和无所适从。
为了解决这个问题,我们进行了深入的调研,并发现公司已经有了一个成熟的生产环境代码覆盖率收集系统。基于这一发现,我们决定将这个系统的数据与我们自己的覆盖率数据相结合,创建了一个 “覆盖率提升优先级列表”。这个列表的目的是为开发团队提供明确的指引,帮助他们了解在哪些方面可以优先提升代码覆盖率。
为了使这个指引更加科学和实用,我们制定了一个覆盖率权重公式:
生产环境覆盖率 ×100×0.3 + (1 – 测试覆盖率)×100×0.3 + 函数数量 ×0.2
通过这个公式,我们能够优先识别出那些生产环境使用率高、行数多,测试覆盖率低的代码文件,从而为开发团队提供针对性的提升建议。这样的方法不仅提高了代码质量,也增强了我们对整体覆盖率的掌控。
十一、社区推广
从这篇文章发表时起,我们将正式开源 Canyon。JavaScript 是时下最流行的编程语言,但是端到端测试覆盖率收集领域一直空白,我们的代码开发基于了 istanbuljs,monaco editor 等优秀开源项目,我们有信心推出 Canyon 开源可以赢得社区的反响,并且可以有大量 JavaScript 开发者参与进来。
Canyon 在未来还有很大发展空间,例如生产环境插桩收集还未有待验证尝试,与 playwright、puppeteer、cypress 等自动化测试的工具还没有深度链接,这些都已经规划到了未来的开发计划中。希望在未来 Canyon 可以在携程及社区里有更多人参与建设。
参考链接
-
开源项目 Canyon:https://github.com/canyon-project/canyon
-
JavaScript 覆盖率工具:https://github.com/istanbuljs/istanbuljs
-
基于浏览器的代码编辑器:https://github.com/microsoft/monaco-editor
-
JavaScript 文本差异:https://github.com/kpdecker/jsdiff
-
“An O(ND) Difference Algorithm and its Variations” (Myers, 1986).http://www.xmailserver.org/diff2.pdf
关于本文
作者:@机票质量工程
原文:https://mp.weixin.qq.com/s/0ExAxquYCb-Rf3X-xwsotA
这期前端早读课
对你有帮助,帮”赞“一下,
期待下一期,帮”在看” 一下 。