多進程下的測試覆蓋率

jopen 8年前發布 | 8K 次閱讀 覆蓋率測試 多進程 測試工具

單元測試在 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
可以看到主進程和子進程中的所有代碼覆蓋率都被統計到了。

來自: http://taobaofed.org/blog/2015/12/15/nodejs-cluster-cov/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!