自动化工具 React Native 代码覆盖率获取探索 (二)

陈恒捷 for PPmoney · June 05, 2017 · Last by Kepler-ZZ replied at June 23, 2020 · 6791 hits
本帖已被设为精华帖!

前文:React Native 代码覆盖率获取探索 (一)

简单回顾一下上一次探索:

  1. 确定了使用覆盖率工具 istanbul 结合 Facebook 的单元测试框架 Jest 可以收集到单元测试覆盖率,但这种方式获取到的只是单元测试覆盖率,并非集成测试覆盖率,运行环境也并非在实际的 react-native app 中。
  2. istanbul-middleware 可以实现独立把 istanbul 嵌入至被测程序中,实时获取集成测试覆盖率,可能类似 jacoco 的 on-the-fly 模式,更符合需要。

接下来,到了打基础的时候了。先了解下 istanbul 到底是个怎样的工具,然后再沿着 istanbul-middleware 的方向继续探索,找到可行的方案

探究 istanbul

istanbul 官方网站:https://istanbul.js.org

主要的组件:

  • nyc: 命令行工具。主要用于把 istanbul 更方便地嵌入到单元测试中,也支持插桩文件生成、覆盖率报告生成等单独功能。
  • babel-plugin-istanbul: babel 插件。提供对 ES6 规范的插桩支持。
  • istanbul-api: istanbul 的公共 api 。经过初步封装,主要用于给外部嵌入 istanbul 。
  • istanbul-lib-coverage: 提供包括合并、汇总及解析覆盖率数据在内功能的 api 。
  • istanbul-lib-hook: 提供对 require, vm.createScript, vm.runInThisContext 三个位置进行自动插桩的钩子方法。
  • istanbul-lib-instrument: 核心模块,负责进行插桩的库。
  • istanbul-lib-report: 覆盖率报告的核心函数库。可以理解为给不同报告生成器使用的公共函数库。
  • istanbul-lib-source-maps: 负责通过 source map 进行覆盖率信息映射的库。(source map 记录实际执行 js 与 js 源码间的映射关系。相关信息建议参考 JavaScript Source Map 详解
  • istanbul-reports: 各种报告生成器。
  • test-exclude: nyc 使用的 include/exclude 逻辑对应的实现库。
  • istanbul-middleware: 用于在功能测试中使用覆盖率的组件,包括脱离单测框架,在代码中直接嵌入覆盖率的钩子方法,以及一个可以收集覆盖率数据后自动生成覆盖率报告的网站应用。

新旧 istanbule 说明:

现在搜索 istanbul 的时候,会发现一些旧的文档和新的文档差异比较大,经过寻找,发现原来 istanbul 中间转过手,所以也把它这段历史简单记录一下。

istanbul 最早的时候,相关的组件都属于 gotwarlost,包括 istanbul 本体、istanbul-middleware 组件等。

但从 0.4.0 版本开始, gotwarlost 不再维护 istanbul ,交给 istanbuljs 继续开发维护。现在大部分使用的 istanbul 就是这个版本。此时 istanbul 大部分组件给了 istanbuljs,但少量组件(如 istanbul-middleware) istanbuljs 并没有接手维护。

目前新的 istanbul 主要针对的是单元测试领域,基本上所有官方文档及实践分享,都是针对如何嵌入到单测中的。而对于功能测试领域,仍然只有 istanbul-middleware 一枝独秀。

istanbul-middleware 探索

把官方的 readme示例项目源码 完全看了一遍,终于大致了解了这个组件是干嘛的了。

istanbul-middleware(后面简称 middleware)本质上是一个基于 express 的网站。但其包含了数个针对 istanbul 覆盖率收集及报告生成的 http 接口,因此可用于作为单独的覆盖率报告生成网站。

URL Description
GET / 动态生成覆盖率 html 报告。和平时单测生成的静态版本一样,可以通过点击逐级深入,查看更细节的覆盖率数据。
POST /reset 把覆盖率数据重置成基线(可以理解成清空当前覆盖率数据)
GET /download 下载一个包含 json 、lcov、html 三种格式覆盖率报告的压缩包
POST /client 用于从浏览器主动发送覆盖率对象。覆盖率对象必须是 json 格式,且发送时 header 中必须有 Content-type: application/json 。这个对象需要和当前服务端已有的统计数据保持一致。补充:即不能把不同程序的覆盖率数据都一起发给同一个 middleware 服务端。
  • 覆盖率收集的两种方式

middleware 支持 server 端及 browser 端的覆盖率数据收集。

server 端
通过 hook require 方法,自动在运行时给 server 端文件插桩。同时添加 /coverage 路径的 handler ,处理上述的覆盖率接口请求。
核心方法:im.hookLoader(__dirname);app.use('/coverage', im.createHandler());
强烈建议看下官方 test/app 文件夹中的示例程序,看完会有更清晰的了解。

browser 端
middleware 只能通过上述的 POST /client 收集覆盖率数据并生成报告,覆盖率数据的上传需要由 browser 端自行处理。即 browser 端的 js 文件需要预先进行插桩再给浏览器运行,并加入定时回传 window.__coverage__ 对象(即覆盖率数据)给 middleware 。

核心方法:

  1. middleware 端添加针对浏览器使用 js 的 handler :app.use(im.createClientHandler(__dirname));,使得所有浏览器获取的 js 文件都是插桩后的文件。
  2. js 文件端添加定时回传覆盖率数据的方法(每隔2秒自动回传,fetch 方法是 react native 提供的网络请求方法):
setInterval(function(){
fetch('http://localhost:8889/coverage/client', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(window.__coverage__)
}).then((response) => console.log(response.json()))
}, 2000);

插桩原理小结

参考 istanbul代码覆盖率工具研究 - Teazean ,以及 middleware 的用法,简单总结下 instanbul 这个工具的插桩方式:

image_1bhpoiqht1j748igru1fq31me89.png-69.2kB

主要有几种。

  • 【server】代码添加 hook:使用 middleware ,在项目入口 js 添加 middleware 的 handler ,给所有 require 函数添加钩子,在 require 时自动进行插桩。middleware 的示例项目里面用的就是这种方法。主要适用于集成测试。
  • 【server】自动插桩:使用 nyc (istanbul 的命令行工具)结合各种单测框架,自动在运行单测时给所有 js 文件进行插桩(具体是在解析器层插桩,还是接近 middleware 的方式,暂时未探究到),无需改动任何源码。主要适用于单元测试。
  • 【client】手动插桩:使用 nyc instrument 命令,把正常 js 变成插桩后 js ,然后再把这个 js 放到浏览器中运行。主要适用于在浏览器或 react native 这类无法实现运行时插桩的场景,既可用于单测(需自行解决覆盖率数据返回给单测框架的问题),也可用于集成测试。接近于 jacoco 的 offline 方式。
  • 【client】自动插桩:使用 middleware 的 createClientHandler ,把指定目录的所有 js 请求(浏览器才会请求 js ,服务端都是 require 来使用 js 的)都返回插桩后的 js 文件。

从这几种插桩方式看出,对于获取类似 react-native 这样运行环境下的覆盖率数据,必须使用运行前插桩的模式,并把覆盖率数据以某种形式返回给后端进行数据解析及报告生成。

普通网站实践

好了,基础基本都学好了,可以开始再次启航了~

一开始,先不要那么难直接挑战 react-native 。我们先做个小 demo ,尝试让 middleware 收集来自 browser 端的覆盖率数据吧。

主要修改步骤:

  1. 对 client.js 添加定时回传覆盖率数据的函数
  2. 对 js 代码进行插桩
  3. 修改 middleware 中源码目录指向

具体代码修改内容已上传 github ,一个步骤对应一次提交 :https://github.com/chenhengjie123/middleware-browser-coverage-demo

PS:实际上第二、第三步可以忽略不做,因为 createClientHandler 本身已经完成了自动插桩的功能。但为了提前给后面的 RN 进行试验(RN 采用的是所有 js 打包成一个文件后再请求,此时 middleware 这种方式就无效了),所以加上第二、第三步。

React-Native APP 实战

终于来到重点了。其实根据前面的普通网站实践,RN 基本也是差不多的套路了。主要的不同点,在于我们还需要自己搭建好 middleware 后台服务。

继续以 f8app 为例,加入覆盖率收集服务。f8app 开发环境具体搭建过程请查看 React Native 代码覆盖率获取探索 (一),此处不再详述。

所有源码均放在了 https://github.com/chenhengjie123/f8app_coverage_demo 上,不关心过程的同学可以直接上去根据 readme 运行 demo 。后面只说关键代码,非关键部分请直接查阅 github 源码。

  • 建立 middleware 后台服务

其实在前面的 middleware 示例项目里,middleware 后台服务的主要源码都已经给出了,我们仿照它的格式,把多余部分去掉就好。修改后的 index.js 文件内容如下:

var express = require('express'),
im = require('istanbul-middleware'),
isCoverageEnabled = true,
app = express(),
port = 8889;

// add the coverage handler
console.log('Coverage reporting at /coverage');
app.use('/coverage', im.createHandler({ verbose: true, resetOnGet: true }));

console.log('Starting server at: http://localhost:' + port);
app.listen(port);

为了方便,我们把 index.js 另外放到一个名为 f8app_coverage_middleware 的目录,与 f8app 平级。同时也补充上对应依赖库的 package.json 文件。

目前目录结构如下:

.
├── f8app
│   ├── LICENSE
│   ├── README.md
│   ├── android
│   ├── index.android.js
│   ├── index.ios.js
│   ├── ios
│   ├── js
│   ├── logs
│   ├── node_modules
│   ├── npm-shrinkwrap.json
│   ├── package.json
│   ├── scripts
│   └── server
└── f8app_coverage_middleware
├── index.js
├── package.json
└── start_middleware.sh
  • 代码添加自动回传覆盖率
// 拷贝源码到 middleware 目录
$ cp -r f8app/js f8app_coverage_middleware
$ cd f8app_coverage_middleware

然后在 f8app_coverage_middleware/js/setup.js 尾部进行如下修改,实现每隔两秒回传覆盖率数据:

--- a/f8app_coverage_middleware/js/setup.js
+++ b/f8app_coverage_middleware/js/setup.js
@@ -84,4 +84,18 @@ global.LOG = (...args) => {
return args[args.length - 1];
};

+// post window.__coverage__ to server every 2 seconds
+setInterval(function() {
+ fetch('http://localhost:8889/coverage/client', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(window.__coverage__)
+ })
+ .then(function() {
+ console.log("success!")
+ })
+}, 2000);
+
module.exports = setup;
  • 代码插桩

接下来,我们需要把 f8app 的 js 都通过命令进行插桩。

// 插桩后代码输出到 f8app/js 目录,覆盖原有内容
$ nyc instrument js ../f8app/js

为了方便后续更新插桩文件,建了个 instrument_js.sh 做到一键更新插桩文件。

  • 启动覆盖率后台服务
$ cd f8app_coverage_middleware && ./start_middleware.sh
  • 启动 react-native ios 客户端(记得先启动好 f8app 运行环境所需的程序)
$ cd f8app && react-native run-ios

待 ios 客户端启动完毕后,等待约2秒,然后打开 http://localhost:8889/coverage 即可查看覆盖率报告。

目前方案已知问题及解决方案

由于 middleware 默认识别相对路径,会造成查看具体文件行级别覆盖率时报类似如下的错误:

Error: ENOENT: no such file or directory, open 'actions/installation.js'
at Error (native)
at Object.fs.openSync (fs.js:549:18)
at Object.fs.readFileSync (fs.js:397:15)
...

原因:middleware 文件解析是根据 url 中的 p 参数值进行解析的。由于 client 只会回传相对路径,因此 middleware 会找不到对应文件。将其生成 html 页面时使用的路径改为绝对路径即可。

解决方法:
打开 f8app_coverage_middleware/node_modules/istanbul-middleware/lib/core.js ,添加下面 + 号开头的代码(实际添加时不需要加上这个 + 号):

        fileCoverage = coverage[outputNode.fullPath()];

+ // 临时修复 `no such file or directory` 报错问题
+ var path = require('path');
+ fileCoverage.path = path.resolve(__dirname, '..', '..', '..', 'js', fileCoverage.path);

utils.addDerivedInfoForFile(fileCoverage);
report.writeDetailPage(res, outputNode, fileCoverage);

小结

断断续续搞了几周,这篇文章也改了3版,终于把 demo 基本搞定了。在这过程中走过不少弯路,例如一开始是把 middleware 也加入到 f8app 项目,直接在 f8app 的 js 里加 handler,结果发现由于依赖 express ,在 rn 运行环境下完全启动不了。因此沉下心,认真把 istanbul 啃了下来,总算找到了一条正确的路,完成了覆盖率的收集。

回头看了下,沿途风景还是不错的。这里也以自己的这段经历提醒下大家:真的不要一上来就按照自己的思路写代码呀。沉下心,打好基础,你能收获比一个可用的程序更多的知识。

下一步就是如何将其工程化,加入到覆盖率平台里了。这部分后续做完再进行分享。

参考地址:
多进程下的测试覆盖率
istanbul代码覆盖率工具研究 - Teazean

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 29 条回复 时间 点赞

@Lihuazhang rn 手工测试的覆盖率也能收集到咯。

陈恒捷 回复

@vigossjjj 我们现在也支持么?

恒温 回复

哇塞,原来 @vigossjjj 到支付宝啦。

陈恒捷 回复

嗯 主攻覆盖率

恒温 回复

好奇的点击来,竟然还被@了,H5覆盖率方案是有的,但是目前没有资源做,排期咯

思寒_seveniruby 将本帖设为了精华贴 06 Jun 11:04

厉害呀,恒捷。
我在厂内貌似没找到RN 覆盖率的文章。而且厂内好像现在WEEX这一块适用范围比较广一点!

testly 回复

这个方案从原理上说, weex 应该也能支持的。

点赞,期待合并到覆盖率平台上。

陈恒捷 如何量化测试覆盖率 中提及了此贴 14 Sep 07:35

你好 请教个问题,如果将 RN 打成 jsbundle 包还能支持收集覆盖率吗

陈恒捷 #12 · June 06, 2018 作者
NAISI 回复

没尝试过,不大确定。理论上只要打包前有做好插桩,还是可以收集到覆盖率的。

您好想问下,我使用babel-istanbul-plugin进行插桩,按照官方操作后,输入window.coverage命令,发现就只有一个文件有覆盖率数据,其他文件都没有。这是什么原因?

陈恒捷 #14 · May 06, 2019 作者
6dingdong6 回复

信息量有点少,不好定位。

建议你单独发帖把完整步骤及相关日志补充下?

请问如果是vue项目要如何插桩呢?

Yangjunfengss 回复

可以试试文中的套路?如果是有具体哪个点卡住了可以具体发出来。

@chenhengjie123 横捷,打扰了,我按照你的文章,写了一个简单的APP。
我的代码上传到了git: https://github.com/OnTheWay111/AwesomeProject.git
看log可以正常上传覆盖率成功,但是没有发现覆盖率数据,可以帮忙看看吗?

覆盖率HTTP页面:

手机模拟器以及log:显示提交覆盖率数据成功

原因是:window.coverage undefined,具体原因还未找到
可能原因:计数器插入失败

Author only

nyc插桩ReactNative的js文件失败的问题解决了,主要是nyc的版本问题,降版本到14.1.1可以了。

注意:package.json直接指定nyc版本和全局nyc版本的区别,如果是直接运行nyc instrument,用的是全局版本

问题解决了,解决方法如下,特别感谢 @zsx10110
我之前使用的降版本方式是package.json中修改版本号,然后npm install , 然后nyc instrument插桩,注意:此时使用的nyc是全局nyc,并不是node_modules中的nyc,所以用的还是全局版本的nyc,导致降版本无效;
感谢@zsx10110 大大,成功帮我解决了,使用如下命令,强制降版本全局nyc:

npm install -g nyc@14.1.1 --registry https://registry.npm.taobao.org

zailushang ReactNative 多端代码覆盖率调研及实践 中提及了此贴 20 May 19:13

你好,我想请问一下,如果js文件中包含typeScript,istanbul是不是就无法获取覆盖率了?

陈恒捷 #23 · June 17, 2020 作者
Kepler-ZZ 回复

好久没做这块相关的工作了,不大确定。 @zailushang 帮忙看下有没有这个限制?

陈恒捷 回复

这块我没继续做了,其他同事在做。
@zsx10110 大佬帮忙看下?

Kepler-ZZ 回复

能不能提供更详细的信息 我们这边typescript的插桩是可以的,并且有能够获取到覆盖率数据,但是我们这边都是前端项目,不是针对react-native的。

saii 回复

感谢~没事,我就是想确认一下,因为一开始是用JSCover,发现无法对TypeScript进行插桩,于是改用Istanbul,我这边也是前端项目,我现在阻塞在回传函数这一步,浏览器一直报错: 求大佬指点,网上百度了一些方法,不知具体道怎么操作。😭

tv.html:1 Access to XMLHttpRequest at 'http://localhost:8088/coverage/client' from origin 'https://localhost:8081' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Kepler-ZZ 回复

这个是前端的跨域请求了 你可以改下你那个8088的服务,让他允许跨域请求就可以了。

saii 回复

这个8088的服务就是istanbul-middleware,网上说添加下面这条语句就行,但是我不知道要加在哪个位置😢

Kepler-ZZ 回复

https://www.jianshu.com/p/fc038a92d1eb 看看这个文章 网上比较多 关于express设置跨域的方式

saii 回复

谢谢大佬,解决了

saii 回复

但是产出的报告有很多问题诶,代码行总数对不上,一共5万多行代码,行覆盖那里显示只有2万行,且未标注覆盖情况。不知道是怎么回事,求大佬指点😭

展示问题已解决,浏览器的原因,使用chrome无痕浏览打开就是黑白,换成普通模式浏览打开就正常。

但是行覆盖总数仍然对不上

数据问题我大概知道了,覆盖率工具只统计有效代码行,会去除注释、空行这些吧。应该是这个原因吧,所以报告上的代码行数是一定会小于等于实际显示的总行数

Kepler-ZZ 回复

我有个问题呀,你说的5w多的代码是指的你的源码吗?然后2w多是指的插桩后的报告吧。这个报告能够对应到你真正的源码不?
我看到你的报告中的vplayer看上去不像是你们的源码呀

saii 回复

我们的播放器工程,编译之后生成了vplayer.js文件,我是直接对这个文件进行添加回传函数+插桩。报告和vplayer.js原文件内容能够对应上。5万的数据来自这里,是我一开始理解错了,这里显示的行数并不是真正的有效代码行数,所以2万多的数据是对的。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up