多进程下的测试覆盖率

简介:

原文: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
主进程和子进程中的所有代码覆盖率都被统计到
可以看到主进程和子进程中的所有代码覆盖率都被统计到了。

目录
相关文章
|
Python
【代码片段】利用python多进程进行接口并发测试
利用python多进程进行接口并发测试
198 0
|
9天前
|
人工智能 测试技术 开发者
北大李戈团队提出大模型单测生成新方法,显著提升代码测试覆盖率
【9月更文挑战第27天】北京大学李戈团队在人工智能领域取得重要突破,提出HITS新方法,通过将待测方法分解为多个切片并利用大型语言模型逐个生成测试用例,显著提升代码测试覆盖率,尤其在处理复杂方法时效果显著,为软件开发和测试领域带来新希望。尽管存在一定局限性,HITS仍展示了巨大潜力,未来有望克服限制,推动软件测试领域的创新发展。论文详情见【https://www.arxiv.org/pdf/2408.11324】。
25 6
|
2月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之计算测试用例的分支覆盖率如何解决
SpringBoot单元测试快速写法问题之计算测试用例的分支覆盖率如何解决
|
3月前
|
测试技术
单元测试策略问题之行覆盖率和分支覆盖率之间的问题如何解决
单元测试策略问题之行覆盖率和分支覆盖率之间的问题如何解决
|
3月前
|
测试技术 开发者
单元测试问题之为什么单测覆盖率高的项目模块更易于迭代演进
单元测试问题之为什么单测覆盖率高的项目模块更易于迭代演进
|
3月前
|
测试技术
codereview开发问题之CodeReview关注代码的测试覆盖率问题如何解决
codereview开发问题之CodeReview关注代码的测试覆盖率问题如何解决
|
3月前
|
Java 测试技术 Maven
在Java项目中集成单元测试与覆盖率工具
在Java项目中集成单元测试与覆盖率工具
|
5月前
|
弹性计算 运维 Shell
测试开关机(多进程版)
【4月更文挑战第29天】
44 0
|
Java 测试技术 Maven
Spring Boot 中的测试覆盖率是什么,如何使用
Spring Boot 中的测试覆盖率是什么,如何使用
|
Java 测试技术 Maven
SpringCloud项目编译打包执行单元测试(修复单元测试数量为0)-流水线sonarqube扫描jacoco插件展示覆盖率
SpringCloud项目编译打包执行单元测试(修复单元测试数量为0)-流水线sonarqube扫描jacoco插件展示覆盖率
下一篇
无影云桌面