多进程下的测试覆盖率

简介:

原文:http://taobaofed.org/blog/2015/12/15/nodejs-cluster-cov/

单元测试在 Node.js 项目开发中的重要性就不言而喻了,项目一旦稍微大起来了就经常出现拆东墙补西墙的情况。这边修复了一个 bug,那边又不知道什么时候产生了一个新的 bug,越到后面没有经过完整的测试都不敢随便发布。

代码覆盖率

测试的时候,我们常常关心,是否所有代码都测试到了。这个指标就叫做“代码覆盖率”(code coverage),它有四个测量维度。

  • 行覆盖率(line coverage):是否每一行都执行了?
  • 函数覆盖率(function coverage):是否每个函数都调用了?
  • 分支覆盖率(branch coverage):是否每个 if 代码块都执行了?
  • 语句覆盖率(statement coverage):是否每个语句都执行了?

目前在 Node.js 开发中比较流行的测试覆盖率工具是 Istanbul。

Yet another JS code coverage tool that computes statement, line, function and branch coverage with module loader hooks to transparently add coverage when running tests. Supports all JS coverage use cases including unit tests, server side functional tests and browser tests. Built for scale.

Istanbul 不但可以统计到整个项目的代码覆盖率,还会生成一份漂亮的覆盖率报告,准确的标记出哪些代码没有被覆盖到。

平常我们写的 JS 测试用例大部分都是单进程的场景,下面我们来看一个多进程项目的测试情况又是怎么样的呢?

多进程 demo

先写一个简单的 demo,使用 Mocha 做单元测试,Istanbul 生成测试覆盖率。
下面是整个项目的目录结构。

.istanbul-cluster-demo
|____.gitignore
|____lib
| |____master.js
| |____worker.js
|____package.json
|____test
| |____index.test.js

master.js

'use strict';

const path = require('path');
const childProcess = require('child_process');

let rid = 0;
const service = {};
const requestQueue = new Map();

module.exports = function (ready) {
  const worker = childProcess.fork(path.join(__dirname,'./worker'));

  function send() {
    rid++;
    let args = [].slice.call(arguments);
    const method = args.slice(0,1)[0];
    const callback = args.slice(-1)[0];

    const req = {
      rid: rid,
      method:method,
      args:args.slice(1,-1)
    };

    requestQueue.set(rid,Object.assign({
      callback: callback
    }, req));

    worker.send(req);
  }

  worker.on('message', function(message){
    if (message.action === 'register') {
       message.methods.forEach((method) => {
        service[method] = send.bind(null, method);
       });
       ready(service);
    } else {
      const req = requestQueue.get(message.rid);
      const callback = req.callback;
      if (message.success) {
        callback(null, message.data);
      } else {
        callback(new Error(message.error));
      }
      requestQueue.delete(message.rid);
    }
  });
}

worker.js

'use strict';

const service = {
  add() {
    const args = [].slice.call(arguments);
    return args.slice().reduce(function(a,b) {
      return a+b;
    });
  },

  time() {
    const args = [].slice.call(arguments);
    return new Promise((resolve, reject)=> {
       setTimeout( ()=> {
          const ret = args.slice().reduce(function(a,b) {
                        return a*b;
                      });
          resolve(ret);
       }, 1000);
    });
  }
}

if (process.send) {
  process.send({
    action:'register',
    methods: Object.keys(service)
  });
}

process.on('message', function(message) {
  let ret = { success: false, rid: message.rid };
  const method = message.method;
  if (service[method]) {
    try {
      const result = service[method].apply(service, message.args);
      ret.success = true;
      if(typeof result.then === 'function') {
        return result.then((data)=> {
          ret.data = data;
          process.send(ret);
        }).catch((err)=>{
          ret.success = false;
          ret.error = err.message;
          process.send(err);
        })
      }
      ret.data = result;
    } catch (err) {
      ret.error = err.message;
    }
  }
  process.send(ret);
});

上面的 demo 实现了一个简单的进程间 rpc 功能,master 进程提供接口,worker 进程实现具体的逻辑,并通过进程间通信给 master 调用。

worker 进程 向 master 进程注册了 add 和 time 方法,分别提供相加和相乘的服务。

测试用例

接着使用 Mocha 写一个脚本测试下功能。

index.test.js

'use strict';
const master = require('../lib/master');
const assert = require('assert');

describe('test/index.test.js', function() {
  let service;
  before(function(done) {
    master(function(_service){
      service = _service;
      done();
    });
  });

  it('add should work', function(done) {
    service.add(1,2,3,4,5, function(err, result) {
        assert(result === 1+2+3+4+5);
        done();
    });
  });

  it('time should work', function(done) {
    service.time(1,2,3,4,5, function(err, result) {
        assert(result === 1*2*3*4*5);
        done();
    });
  });
});

运行 node --harmony node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- test/**/*.test.js 输出

测试结果

所有的测试用例都已经跑通并统计出各种覆盖率, 再看看生成的覆盖率报告
覆盖率报告,子进程没有被统计到

发现并没有 worker.js 的覆盖率数据,所以上面输出的覆盖率是不完整的。

分析原因

为什么测试的结果中会没有 worker.js 的覆盖率数据呢,稍微想一下其实很简单,master.js 之所以有覆盖率数据因为它通过 Istanbul 启动执行的,代码运行之前 Istanbul 会对 master.js 进行 instrument。下面是一段代码被 instrument 前后的情况。

before instrument

function test() { return "Node.js"; }

after instrument

var __cov_lgAhQ3cOIwE1WdZw07U4cQ = (Function('return this'))();
if (!__cov_lgAhQ3cOIwE1WdZw07U4cQ.__coverage__) { __cov_lgAhQ3cOIwE1WdZw07U4cQ.__coverage__ = {}; }
__cov_lgAhQ3cOIwE1WdZw07U4cQ = __cov_lgAhQ3cOIwE1WdZw07U4cQ.__coverage__;
if (!(__cov_lgAhQ3cOIwE1WdZw07U4cQ['demo.js'])) {
   __cov_lgAhQ3cOIwE1WdZw07U4cQ['demo.js'] = {"path":"demo.js","s":{"1":1,"2":0},"b":{},"f":{"1":0},"fnMap":{"1":{"name":"test","line":1,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":16}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":1,"column":37}},"2":{"start":{"line":1,"column":18},"end":{"line":1,"column":35}}},"branchMap":{}};
}
__cov_lgAhQ3cOIwE1WdZw07U4cQ = __cov_lgAhQ3cOIwE1WdZw07U4cQ['demo.js'];
function test(){__cov_lgAhQ3cOIwE1WdZw07U4cQ.f['1']++;__cov_lgAhQ3cOIwE1WdZw07U4cQ.s['2']++;return'Node.js';}

可以看出 instrument 后的代码每一行是否被执行都可以监测到,而 worker.js 是 master.js 通过调用 childProcess.fork, 在一个很干净的 Node.js 环境中执行, 执行的代码没有被 instrument,执行情况自然无法被 Istanbul 检测到。

解决方案

所以要获取 worker.js 的覆盖率,必须在执行 worker.js 代码之前先注入 Istanbul,那很自然的就会想到 hack 掉 childProcess.fork。

const childProcess = require('child_process');
const fork = childProcess.fork;
const path = require('path');

childProcess.fork = function(modulePath, args, options) {
  const execPath = path.resolve(__dirname,'../node_modules/.bin/istanbul');
  args = ['cover', '--report', 'none', '--print', 'none', '--include-pid',modulePath+'.js'];
  return fork.apply(childProcess,[execPath, args, options]);
}

虽然这样处理后 master.js 和 worker.js 的覆盖率都有了,但由于它们是在不同的进程中产生的,Istanbul 不会自动将 2 个文件的覆盖率数据合并处理,所以我们可以先产生覆盖率数据,再根据覆盖率数据生成报告。由于涉及到多个进程,启动 Istanbul 时需要加上 include-pid 参数,这样每个进程生成的 coverage.json 文件就会带上进程 pid,否则 子进程的 coverage.json 会覆盖掉 主进程的。

运行 istanbul report --root ./coverage text-summary json lcov 便会自动对生成的 coverage-pid.json 文件合并处理,产生最终的覆盖率数据以及覆盖率报告。

最后将这 2 条命令集成到 Node.js 项目的 package.json 文件中。

"scripts": {
    "test":"npm run cov && npm run report",
    "report":"node --harmony node_modules/.bin/istanbul report --root ./coverage text-summary json lcov",
    "cov": "node --harmony node_modules/.bin/istanbul cover  --report none --print none --include-pid  ./node_modules/mocha/bin/_mocha -- 'test/**/*.test.js'"
  }

执行 npm test
主进程和子进程中的所有代码覆盖率都被统计到
可以看到主进程和子进程中的所有代码覆盖率都被统计到了。

目录
相关文章
|
测试技术
IDEA创建单元测试与测试覆盖率统计
IDEA(IntelliJ IDEA)不仅支持快速基于当前类创建单元测试,还支持代码测试覆盖率的统计,以及生成报告和标记测试运行命中的代码。
1810 0
IDEA创建单元测试与测试覆盖率统计
|
8月前
|
Java 测试技术 Maven
Spring Boot 中的测试覆盖率是什么,如何使用
Spring Boot 中的测试覆盖率是什么,如何使用
|
10月前
|
Java 测试技术 Maven
SpringCloud项目编译打包执行单元测试(修复单元测试数量为0)-流水线sonarqube扫描jacoco插件展示覆盖率
SpringCloud项目编译打包执行单元测试(修复单元测试数量为0)-流水线sonarqube扫描jacoco插件展示覆盖率
|
测试技术
保证测试的覆盖率
保证测试的覆盖率
|
存储 小程序 测试技术
聊聊测试覆盖率
聊聊测试覆盖率
|
安全 NoSQL Java
JAVA实战:如何让单元测试覆盖率达到80%甚至以上
单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证。它是软件测试中的一种基本方法,也是软件开发过程中的一个重要步骤。 单元测试的目的是在于确保软件的每个独立模块都被正确地测试,并且没有潜在的缺陷或漏洞。在单元测试中,需要对每个模块进行测试,以确保它们能够按照预期的方式工作,并且没有任何错误或漏洞。
漫谈测试覆盖率
测试覆盖率和需求挂钩,高度依赖研发过程,需要分阶段执行不同粒度,最终结果和线上交付质量成比例;
|
JavaScript 前端开发 测试技术
webpack配置篇(三十四):单元测试和测试覆盖率
webpack配置篇(三十四):单元测试和测试覆盖率
193 0
webpack配置篇(三十四):单元测试和测试覆盖率
|
测试技术 数据库 Python
查看登录性能优化和测试覆盖率的详细解决方案【flask框架】
查看登录性能优化和测试覆盖率的详细解决方案【flask框架】
113 0
查看登录性能优化和测试覆盖率的详细解决方案【flask框架】
|
敏捷开发 Java 测试技术
【测试基础】四、你的测试覆盖率是多少?
【测试基础】四、你的测试覆盖率是多少?
【测试基础】四、你的测试覆盖率是多少?

热门文章

最新文章