前文: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

主要的组件:

新旧 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

主要有几种。

从这几种插桩方式看出,对于获取类似 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 后台服务的主要源码都已经给出了,我们仿照它的格式,把多余部分去掉就好。修改后的 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
$ 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


↙↙↙阅读原文可查看相关链接,并与作者交流