简单回顾一下上一次探索:
接下来,到了打基础的时候了。先了解下 istanbul 到底是个怎样的工具,然后再沿着 istanbul-middleware 的方向继续探索,找到可行的方案
istanbul 官方网站:https://istanbul.js.org
主要的组件:
require
, vm.createScript
, vm.runInThisContext
三个位置进行自动插桩的钩子方法。新旧 istanbule 说明:
现在搜索 istanbul 的时候,会发现一些旧的文档和新的文档差异比较大,经过寻找,发现原来 istanbul 中间转过手,所以也把它这段历史简单记录一下。
istanbul 最早的时候,相关的组件都属于 gotwarlost,包括 istanbul 本体、istanbul-middleware 组件等。
但从 0.4.0 版本开始, gotwarlost 不再维护 istanbul ,交给 istanbuljs 继续开发维护。现在大部分使用的 istanbul 就是这个版本。此时 istanbul 大部分组件给了 istanbuljs,但少量组件(如 istanbul-middleware)istanbuljs 并没有接手维护。
目前新的 istanbul 主要针对的是单元测试领域,基本上所有官方文档及实践分享,都是针对如何嵌入到单测中的。而对于功能测试领域,仍然只有 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 。
核心方法:
app.use(im.createClientHandler(__dirname));
,使得所有浏览器获取的 js 文件都是插桩后的文件。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 这个工具的插桩方式:
主要有几种。
createClientHandler
,把指定目录的所有 js 请求(浏览器才会请求 js ,服务端都是 require 来使用 js 的)都返回插桩后的 js 文件。从这几种插桩方式看出,对于获取类似 react-native 这样运行环境下的覆盖率数据,必须使用运行前插桩的模式,并把覆盖率数据以某种形式返回给后端进行数据解析及报告生成。
好了,基础基本都学好了,可以开始再次启航了~
一开始,先不要那么难直接挑战 react-native 。我们先做个小 demo ,尝试让 middleware 收集来自 browser 端的覆盖率数据吧。
主要修改步骤:
具体代码修改内容已上传 github ,一个步骤对应一次提交 :https://github.com/chenhengjie123/middleware-browser-coverage-demo
PS:实际上第二、第三步可以忽略不做,因为 createClientHandler
本身已经完成了自动插桩的功能。但为了提前给后面的 RN 进行试验(RN 采用的是所有 js 打包成一个文件后再请求,此时 middleware 这种方式就无效了),所以加上第二、第三步。
终于来到重点了。其实根据前面的普通网站实践,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