测试覆盖率 关于 nodejs 覆盖率的探索

saii · 2021年08月08日 · 最后由 saii 回复于 2021年08月09日 · 3276 次阅读

这个研究起源于这个帖子 如何收集 nodejs 服务端的测试覆盖率(Nodejs), 由于自己之前对这块其实是没有研究过的,所以特地花了些时间去了解了下。

知识储备

虽然说没有做过这块的覆盖率研究,但是对于针对 nodejs 这块的覆盖率工具之前是有做过一定的了解的,主要是在 nyc 以及 istanbul-middleware (以下简称 IM)。

关于两者的一些说明可以去看下以下这两篇文章,里面讲了挺多内容的
探索 istanbul/nyc 代码覆盖工具的原理
React Native 代码覆盖率获取探索 (二)

nyc 的初步尝试

由于 nodejs 这块很少会用到 babel 编译这种方式,所以我们就放弃了之前 聊聊前端代码覆盖率 (长文慎入) 介绍到的 babel-plugin-istanbul。 而是直接采用 nyc 运行前插桩的方式。

这里可以直接查看下这个栗子 nyc-expresss-coverage-demo

其实就是通过 nyc instrument的命令 对相应的代码目录进行插桩操作。 相应的插桩后的文件如:

data.js

module.exports = {
    authors: [
        {
            id: '1',
            name: 'John Irving',
            country: 'USA',
            dob: '03/02/1942'
        },
        {
            id: '2',
            name: 'Gabriel Garcia Marquez',
            country: 'Colombia',
            dob: '03/06/1927'
        },
        {
            id: '3',
            name: 'Salman Rushdie',
            country: 'India',
            dob: '06/19/1947'
        },
        {
            id: '4',
            name: 'Stanislaw Lem',
            country: 'Poland',
            dob: '09/12/1921',
            deceased: true
        }
    ]
};

变为了

function cov_106hx7e2f0() {
  var path = "/Users/sai/projects/nyc-expresss-coverage-demo/server/data.js";
  var hash = "ff103e96a45dea0c0d53a6e72b3c27f5cdf62f87";
  var global = new Function("return this")();
  var gcv = "__coverage__";
  var coverageData = {
    path: "/Users/sai/projects/nyc-expresss-coverage-demo/server/data.js",
    statementMap: {
      "0": {
        start: {
          line: 1,
          column: 0
        },
        end: {
          line: 29,
          column: 2
        }
      }
    },
    fnMap: {},
    branchMap: {},
    s: {
      "0": 0
    },
    f: {},
    b: {},
    _coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
    hash: "ff103e96a45dea0c0d53a6e72b3c27f5cdf62f87"
  };
  var coverage = global[gcv] || (global[gcv] = {});

  if (!coverage[path] || coverage[path].hash !== hash) {
    coverage[path] = coverageData;
  }

  var actualCoverage = coverage[path];
  {
    // @ts-ignore
    cov_106hx7e2f0 = function () {
      return actualCoverage;
    };
  }
  return actualCoverage;
}

cov_106hx7e2f0();
cov_106hx7e2f0().s[0]++;
module.exports = {
  authors: [{
    id: '1',
    name: 'John Irving',
    country: 'USA',
    dob: '03/02/1942'
  }, {
    id: '2',
    name: 'Gabriel Garcia Marquez',
    country: 'Colombia',
    dob: '03/06/1927'
  }, {
    id: '3',
    name: 'Salman Rushdie',
    country: 'India',
    dob: '06/19/1947'
  }, {
    id: '4',
    name: 'Stanislaw Lem',
    country: 'Poland',
    dob: '09/12/1921',
    deceased: true
  }]
};
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImRhdGEuanMiXSwibmFtZXMiOlsibW9kdWxlIiwiZXhwb3J0cyIsImF1dGhvcnMiLCJpZCIsIm5hbWUiLCJjb3VudHJ5IiwiZG9iIiwiZGVjZWFzZWQiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFlWTs7Ozs7Ozs7OztBQWZaQSxNQUFNLENBQUNDLE9BQVAsR0FBaUI7QUFDYkMsRUFBQUEsT0FBTyxFQUFFLENBQ0w7QUFDSUMsSUFBQUEsRUFBRSxFQUFFLEdBRFI7QUFFSUMsSUFBQUEsSUFBSSxFQUFFLGFBRlY7QUFHSUMsSUFBQUEsT0FBTyxFQUFFLEtBSGI7QUFJSUMsSUFBQUEsR0FBRyxFQUFFO0FBSlQsR0FESyxFQU9MO0FBQ0lILElBQUFBLEVBQUUsRUFBRSxHQURSO0FBRUlDLElBQUFBLElBQUksRUFBRSx3QkFGVjtBQUdJQyxJQUFBQSxPQUFPLEVBQUUsVUFIYjtBQUlJQyxJQUFBQSxHQUFHLEVBQUU7QUFKVCxHQVBLLEVBYUw7QUFDSUgsSUFBQUEsRUFBRSxFQUFFLEdBRFI7QUFFSUMsSUFBQUEsSUFBSSxFQUFFLGdCQUZWO0FBR0lDLElBQUFBLE9BQU8sRUFBRSxPQUhiO0FBSUlDLElBQUFBLEdBQUcsRUFBRTtBQUpULEdBYkssRUFtQkw7QUFDSUgsSUFBQUEsRUFBRSxFQUFFLEdBRFI7QUFFSUMsSUFBQUEsSUFBSSxFQUFFLGVBRlY7QUFHSUMsSUFBQUEsT0FBTyxFQUFFLFFBSGI7QUFJSUMsSUFBQUEsR0FBRyxFQUFFLFlBSlQ7QUFLSUMsSUFBQUEsUUFBUSxFQUFFO0FBTGQsR0FuQks7QUFESSxDQUFqQiIsInNvdXJjZXNDb250ZW50IjpbIm1vZHVsZS5leHBvcnRzID0ge1xuICAgIGF1dGhvcnM6IFtcbiAgICAgICAge1xuICAgICAgICAgICAgaWQ6ICcxJyxcbiAgICAgICAgICAgIG5hbWU6ICdKb2huIElydmluZycsXG4gICAgICAgICAgICBjb3VudHJ5OiAnVVNBJyxcbiAgICAgICAgICAgIGRvYjogJzAzLzAyLzE5NDInXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICAgIGlkOiAnMicsXG4gICAgICAgICAgICBuYW1lOiAnR2FicmllbCBHYXJjaWEgTWFycXVleicsXG4gICAgICAgICAgICBjb3VudHJ5OiAnQ29sb21iaWEnLFxuICAgICAgICAgICAgZG9iOiAnMDMvMDYvMTkyNydcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgICAgaWQ6ICczJyxcbiAgICAgICAgICAgIG5hbWU6ICdTYWxtYW4gUnVzaGRpZScsXG4gICAgICAgICAgICBjb3VudHJ5OiAnSW5kaWEnLFxuICAgICAgICAgICAgZG9iOiAnMDYvMTkvMTk0NydcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgICAgaWQ6ICc0JyxcbiAgICAgICAgICAgIG5hbWU6ICdTdGFuaXNsYXcgTGVtJyxcbiAgICAgICAgICAgIGNvdW50cnk6ICdQb2xhbmQnLFxuICAgICAgICAgICAgZG9iOiAnMDkvMTIvMTkyMScsXG4gICAgICAgICAgICBkZWNlYXNlZDogdHJ1ZVxuICAgICAgICB9XG4gICAgXVxufTtcblxuXG4iXX0=

如果了解一定的逻辑的话就会发现其实下来 node 服务执行以后,就可以通过global['__coverage__'] 获取到相应的覆盖率数据。 再然后的内容就不继续说了,可以看下前面提到的知识储备中的文章,怎么去生成报告。 或者看下 demo 中的步骤也是可以的。

IM 的运行中插桩的尝试

在上述的 nyc 的怎么过程中我们发现整体的过程很不方便,首先是是编译前插桩这个动作,再来就是覆盖率需要自己采集以及报告生成。

所以我们这个时候可以考虑下 IM 了。

IM 提供了以下的功能

  • hook 了require()的方法,实际上就是被 require 的文件都会插桩。
  • 可以通过接口导出覆盖率的数据
  • 允许重置覆盖率的数据
  • 允许用户下载覆盖率的报告

这里就不举例说明怎么使用 IM 的了,因为他的说明文档已经很详细了,关键就是使用 im.hookLoader() 这个 api 就可以了。

不过这里有一个需要注意的地方, IM hook 的是require()的方法, 所以这个会导致一个问题就是引入 IM 库的那个文件没办法被插桩, 这个是需要注意的地方 (这里是个人尝试总结的,如果有哪里不对的,欢迎批评指出)。

另外在使用 IM 的时候还发现另外的一个问题, 那就是 IM 针对 es6 的插桩是存在问题的,类似于这个 issue Updating Instrumentor (so that it supports ES6) ,其实只要更换 instrument 的库即可,不过作者已经不维护了,所以代码也一直未合并。

nyc 运行中插桩

经过上述的一些尝试, 又重新把目光放回了 nyc 上,既然 istanbul 已经不维护,而转到了 nyc 上,nyc 这块肯定不单单只是运行前插桩这种方式的。所以尝试又重新在网上搜索了下,结果真的有了新的发现。 nodejs 测试覆盖度工具 nyc(Istanbul) 简介 在这篇文章中作者执行 nyc 的方式并不是与 mocha 等测试框架结合使用的。而是直接在启动服务命令增加了 nyc。

带着这个希望,重新尝试了下,发现在启动命令前 加上 nyc 执行以后,等到服务停止执行后,就可以在coverage 目录下生成覆盖率的报告了, 只是这里有一点不好的就是一定要服务停止以后才能够生成报告。

所以我们需要方便些的方式: 就是支持实时动态生成覆盖率报告的, 所以就有了 nodejs-coverage-lib 这个库, 其实这个库的功能还是相当的简单的,就是通过 express 启动了一个服务,提供了一个报告下载的接口, 而其中的过程则是获取到全局的变量__coverage__ 存储到对应的目录下后,再直接通过 nyc 命令生成报告,压缩最后的报告目录提供下载即可。

总结

以上就是关于 nodejs 这块覆盖率的一些调研以及总结。

  1. 通过nyc运行前插桩的方式进行获取到覆盖率数据,只是步骤有点小麻烦
  2. IM 的运行时插桩的方式。只是可能存在 es6 插桩的问题以及入口文件无法获取到覆盖率的问题
  3. nyc 运行时插桩的方式目前来看是最推荐的方式了。并且通过文中的第三方库,可以省去很多过程中的步骤。

nodejs 这块的覆盖率这块相对于前端来说还是会方便很多,因为主要还是覆盖率的数据就是在服务端处,免去了很多在用户端采集的过程了。

PS: 这里的覆盖率验证只是找了两个项目做了尝试,并不一定适用所有的项目 (比如说 typescript 的项目)。

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

多项目共用一个 im,覆盖率数据要怎么隔离?

saii #2 · 2021年08月09日 Author
冯先生 回复

能够说下具体的场景吗?

saii 回复

就是多个前端项目,同时把覆盖率的数据上报到 im,这个时候 im 上面是展示所有代码路径的覆盖率,有没有办法区分起来

saii #1 · 2021年08月09日 Author
冯先生 回复

哦 我懂了,这个肯定是需要你自己二次开发 im 的, 我们这边的前端代码覆盖率没有用到 im, 是自己根据 cypress 的code-coverage 做的二次开发同时我们是跟运维的同学做了合作,在包部署平台使用容器的 sidecar 的方式,拦截了所有的请求,并且往页面中注入定时上报的代码,上报的内容就就包含了对应的项目名称等,这样子我们的覆盖率服务(你可以理解为 im)就可以知道是那个应用的服务率数据了。 关于前端代码覆盖率的实现 我可以找时间分享下我们这边的一些内容

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册