前端精准测试探索:覆盖率实时统计工具

背景

随着业务增长, 随之而来的前端需求激增, 如何在有限的时间内保证前端代码的质量.

通过测试同学单方面的保障, 还是免不了前端线上问题, 存在回归不到位或者测试遗漏的地方, 同时测试质量的高低没有客观数据可量化.

通过单测方法补充, 可以提前发现一部分问题, 减少问题解决的成本, 但是由于业务形态的原因, 需求变更频繁, 功能迭代快, 补充和维护单测的成本比较高, 在业务方的大部分前端工程中暂时没有单测方法, 因此开发在自测时, 感知比较薄弱, 无量化数据, 在项目提测前也没有统一指标可以把关, 测试对开发的自测状况也不了解;

同时前端缺少像jacoco这样的集成测试覆盖率统计框架, 无法通过集成测试收集覆盖率, 对于测试阶段的质量仍然没有数据量化

结合上面说的几点, 我们提出了前端集成测试覆盖率统计工具的需要, 以此来提升开发自测质量以及项目提测质量, 同时帮助补充回归不到位或测试遗漏的场景, 提升上线质量.

技术选型

首先, 覆盖率收集的前提, 需要完成代码插桩工作, 插桩方法来自于两个开源覆盖率统计框架, istanbul.js以及istanbul-middleware (以下称im) , 提供了若干个插桩方法, 而im其实也是在istanbul.js的基础上做了封装, 能力来自于istanbul-lib-instrument

所有的插桩方法, 大致分为两种类型 —— 1、运行前插桩 2、运行时插桩

运行前插桩

nyc instrument

针对编译之后的JS文件 , 进行手动插桩 , 形成插桩后的新JS文件

babel-plugin-istanbul

istanbul提供的babel插件 , 能够在代码编译打包阶段直接植入插桩代码

适用于使用babel的前端工程,基于react和vue的工程都可以

运行时插桩

im.hookLoader

适用于服务端的文件挂载 比如node应用

当应用启动时 , 会在require入口处添加hook方法 , 使得应用启动时加载到的都是插桩后的代码

im.createClientHandler

适用于客户端的JS挂载 ,比如react和vue的js

通过指定root路径,会把所有该路径的js文件请求拦截,返回插桩后的代码,即浏览器请求静态资源的动作

效果与babel-plugin-istanbul类似,区别在于该方法是在浏览器请求js时才会返回插桩代码,是一个动态过程

插桩方式 功能 优势 劣势
nyc 本地手动插桩源js文件, 生成插桩后文件 编译后的js都可手动插桩, 不限工程框架 手动插桩后的文件需要自己上传, 对原打包发布流程有影响; 不适用于服务端插桩
babel-plugin-istanbul 在babel编译时 , 自动生成插桩代码 改造成本低 , 自动插桩快捷 限定于使用babel的工程
im.hookLoader require入口处添加钩子方法,返回已插桩代码 改造成本低 , 自动插桩快捷 仅适用于服务端插桩
im.createClientHandler 拦截浏览器请求静态资源文件的GET方法, 返回插桩后的JS 自动插桩 , 无须改造原打包流程和脚本 仅适用于客户端插桩; 该方法基于express, 限定于使用express的工程

最后我们所使用的插桩方法

App(node)—— istanbulMiddleware.hookLoader

Client(react & vue)—— babel-plugin-istanbul

模块设计

主要分为三个模块 , 先通过代码插桩获得可追踪的代码 , 然后实时上报用户行为产生的代码行覆盖记录 , 最后呈现覆盖率相关信息.

代码插桩

Client端

使用babel-plugin-istanbul插件, 在babel编译阶段即可完成

Node端

依赖istanbuljs提供的能力 – istanbul-lib-hook 、istanbul-lib-instrument

重写istanbulMiddleware.hookLoader方法 , node启动前挂载文件 , 会在require方法前增加钩子函数, 增加代码插桩

插桩结果举例

被插桩的JS 会新增一个coverage方法, 维护并指向覆盖率信息归属, 并用来获取该文件的覆盖率信息

同时该js中的方法在执行过程的路径上会留下标记, 被执行到之后实时更新覆盖率信息中相对应的行或者块信息

数据上报

Node端

应用发布时 , 写入对应的工程和分支信息 , 创建定时器 , 实时上传_global.coverage变量 , 即覆盖率信息

Client端

客户端的上报比较特殊 , 客户端不像服务端 , 在发布后可以全局保持coverage变量以及定时器方法 , client端所有的数据生成和消耗都跟随页面的生命周期 , 所以不太可控 , 因此需要一个额外容器进行处理 , 我们选择了通过chrome插件来上报 , 通过全局管理覆盖率信息对象 , 以及通知下发来实现上报流程 . 该插件详细的工作流程如下

覆盖率服务端

  • 继承istanbul middleware的功能
  • 支持分支维度接收和查询覆盖率
  • 代码变更时覆盖率替换, 支持存储和查看历史版本

主要基于istanbul-middleware做了二次开发 , 扩展了istanbulMiddleware.createHandler方法

/:ns/:repo /:ns/:repo/show

两个覆盖率展示接口 新增了ns、repo、branch三个入参, 用来区别不同的覆盖率

同时增加额外参数history 传入该变量 标志获取的是历史覆盖率

/client/:ns/:repo?branch={}&source={} body携带覆盖率信息

根据应用和分支信息上报 接收到上报信息之后 会先获取该分支下的已有覆盖率 然后和此次上报的信息做合并

合并是根据文件名字遍历合并的 如果发现某个文件 新旧两份覆盖率结构不同

即发生了代码变更 则会丢弃旧的覆盖率 以新覆盖率为准 同时把旧的覆盖率存储到历史版本中

页面展示

全量覆盖率展示

使用istanbulmiddle原生报告生成

增量覆盖率展示

通过gitlab接口对比master差异 , 分文件展示各自的覆盖率 , 同全量覆盖率 , 只是细化了 , 整体页面用vue + muse-ui完成

以master分支为基准, 增量文件覆盖率

全量文件覆盖率目录结构

工作流程

主要分为3部分 , 对应上一节说的接入 、上报 、展示

通过babel插件完成客户端代码插桩 , 提供给node端使用的插桩插件 , 可以一步完成服务端的代码插桩以及定时上报功能

配合提供的chrome插件 , 完成客户端的覆盖率上报任务

覆盖率服务端负责信息的接收和存储 , 并返回给前端数据信息

前端负责数据信息展示

业务实践

接入测试环境发布平台, 实现以应用和分支维度的多版本实时覆盖率收集和展示功能 , 无缝对接项目测试环境 , 项目中各应用发布后 , 自动开启覆盖率上报 , 在项目测试过程中实时记录 , 可实时查看. 在项目提测前 , 给予开发量化指标 , 项目测试结束后可以给出最终覆盖率数据 , 帮助测试同学检查以及完善未覆盖的功能.

目前在电商教育和行业两条业务线中已有接入,由于该工具限制在qa环境使用 , 仅限收集在qa环境测试的项目数据. 在功能测试阶段,从使用数据上来看 , 增量行代码覆盖率达到80%以上(目前的增量只到文件维度 , 未到行维度), 未覆盖的行主要包括四种: 异常捕获、防御性编码、非本次新增无需关心的代码以冗余代码 , 属于可允许的范围.