多进程下的测试覆盖率

简介:

原文: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多进程进行接口并发测试
220 0
|
2月前
|
安全 测试技术
北大李戈团队提出大模型单测生成新方法,显著提升代码测试覆盖率
【10月更文挑战第1天】北京大学李戈教授团队提出了一种名为“统一生成测试”的创新方法,有效提升了大模型如GPT-2和GPT-3在单一测试中的代码生成覆盖率,分别从56%提升至72%和从61%提升至78%。这种方法结合了模糊测试、变异测试和生成对抗网络等多种技术,克服了传统测试方法的局限性,在大模型测试领域实现了重要突破,有助于提高系统的可靠性和安全性。然而,该方法的实现复杂度较高且实际应用效果仍需进一步验证。论文可从此链接下载:【https://drive.weixin.qq.com/s?k=ACAAewd0AA48Z2kXrJ】
70 1
|
3月前
|
人工智能 测试技术 开发者
北大李戈团队提出大模型单测生成新方法,显著提升代码测试覆盖率
【9月更文挑战第27天】北京大学李戈团队在人工智能领域取得重要突破,提出HITS新方法,通过将待测方法分解为多个切片并利用大型语言模型逐个生成测试用例,显著提升代码测试覆盖率,尤其在处理复杂方法时效果显著,为软件开发和测试领域带来新希望。尽管存在一定局限性,HITS仍展示了巨大潜力,未来有望克服限制,推动软件测试领域的创新发展。论文详情见【https://www.arxiv.org/pdf/2408.11324】。
110 6
|
2月前
|
设计模式 关系型数据库 测试技术
进阶技巧:提高单元测试覆盖率与代码质量
【10月更文挑战第14天】随着软件复杂性的不断增加,确保代码质量的重要性日益凸显。单元测试作为软件开发过程中的一个重要环节,对于提高代码质量、减少bug以及加快开发速度都有着不可替代的作用。本文将探讨如何优化单元测试以达到更高的测试覆盖率,并确保代码质量。我们将从编写有效的测试用例策略入手,讨论如何避免常见的测试陷阱,使用mocking工具模拟依赖项,以及如何重构难以测试的代码。
67 4
|
5月前
|
测试技术
单元测试策略问题之行覆盖率和分支覆盖率之间的问题如何解决
单元测试策略问题之行覆盖率和分支覆盖率之间的问题如何解决
174 7
|
4月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之计算测试用例的分支覆盖率如何解决
SpringBoot单元测试快速写法问题之计算测试用例的分支覆盖率如何解决
|
5月前
|
测试技术 开发者
单元测试问题之为什么单测覆盖率高的项目模块更易于迭代演进
单元测试问题之为什么单测覆盖率高的项目模块更易于迭代演进
|
5月前
|
测试技术
codereview开发问题之CodeReview关注代码的测试覆盖率问题如何解决
codereview开发问题之CodeReview关注代码的测试覆盖率问题如何解决
|
5月前
|
Java 测试技术 Maven
在Java项目中集成单元测试与覆盖率工具
在Java项目中集成单元测试与覆盖率工具
|
7月前
|
弹性计算 运维 Shell
测试开关机(多进程版)
【4月更文挑战第29天】
52 0