React Native 代码覆盖率获取探索 (二)

random
random
random
订阅者
10318
文章
0
粉丝
测试交流42413字数 3106阅读10分21秒阅读模式

前文: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 接口,因此可用于作为单独的覆盖率报告生成网站。

  • 覆盖率相关接口信息(基础路径为 coverage ,如重置覆盖率数据,需要访问的路径是 http://localhost/coverage/reset ):
URLDescription
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 这个工具的插桩方式:

React Native 代码覆盖率获取探索 (二)

主要有几种。

  • 【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

 
评论  42  访客  42
    • 在路上
      在路上 9

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

      • 陈恒捷
        陈恒捷 9

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

        • Kepler-ZZ
          Kepler-ZZ 9

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

          • 在路上
            在路上 9

            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

            • yinwenchang
              yinwenchang 9

              仅楼主可见

              • 在路上
                在路上 9

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

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

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

                • 陈恒捷
                  陈恒捷 9

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

                  • Yangjunfengss
                    Yangjunfengss 9

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

                    • 陈恒捷
                      陈恒捷 9

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

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

                      • 6dingdong6
                        6dingdong6 9

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

                      匿名

                      发表评论

                      匿名网友
                      确定

                      拖动滑块以完成验证