Posted in

【第 3287 期】Canyon: 提升 JavaScript 代码质量的全面覆盖率分析工具_AI阅读总结 — 包阅AI

包阅导读总结

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

这期前端早读课
对你有帮助,帮”
“一下,
期待下一期,帮”
在看” 一下 。