Mastering Multi‑Process Code Coverage in Node.js with Istanbul

Learn how to achieve comprehensive code coverage for multi‑process Node.js applications by using Istanbul, instrumenting child processes, writing Mocha tests, and merging coverage reports, with detailed examples of master‑worker RPC, custom fork hacks, and package.json scripts for automated testing.

Node Underground
Node Underground
Node Underground
Mastering Multi‑Process Code Coverage in Node.js with Istanbul

Unit testing is essential in Node.js projects, especially as they grow and bugs become harder to track.

Code Coverage

When testing, we often wonder whether every line of code has been exercised. This metric, called code coverage, has four measurement dimensions:

Line coverage: has every line been executed?

Function coverage: has every function been called?

Branch coverage: has every if block been executed?

Statement coverage: has every statement been executed?

In Node.js development, the most popular coverage tool is 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 not only reports overall project coverage but also generates a detailed report highlighting uncovered code.

Multi‑process Demo

Below is a simple demo using Mocha for unit testing and Istanbul for coverage, illustrating how to handle a multi‑process Node.js project.

.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);
});

This demo implements a simple inter‑process RPC where the master process exposes interfaces and the worker process provides the actual logic via IPC.

The worker registers add and time methods, offering addition and multiplication services.

Test Cases

Using Mocha, we write tests for the functionality.

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();
    });
  });
});

Running the coverage command produces a report, but initially the worker.js file shows no coverage data.

Because the worker process is forked without instrumentation, its execution is invisible to Istanbul.

Analyzing the Cause

Istanbul instruments the code before execution; the master process is started via Istanbul, so its code is instrumented. The worker, however, runs in a clean Node.js environment, so its code is not instrumented and coverage cannot be collected.

Before instrumentation: function test() { return "Node.js"; } After instrumentation:

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}}}};
}
__cov_lgAhQ3cOIwE1WdZw07U4cQ = __cov_lgAhQ3cOIwE1WdZw07U4cQ['demo.js'];
function test(){
  __cov_lgAhQ3cOIwE1WdZw07U4cQ.f['1']++;
  __cov_lgAhQ3cOIwE1WdZw07U4cQ.s['2']++;
  return "Node.js";
}

The instrumented code tracks execution of each line, but the worker process lacks this instrumentation.

Solution

To collect coverage for worker.js, we must inject Istanbul before the worker is executed, which can be done by hijacking 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]);
};

After this change, both master and worker coverage data are generated, but because they run in separate processes, Istanbul does not merge them automatically. We first generate coverage data, then combine the reports.

Since multiple processes are involved, the --include-pid flag must be used so each process writes its own coverage‑pid.json file, preventing overwrites.

Running istanbul report --root ./coverage text-summary json lcov merges the per‑process files and produces the final coverage report.

These commands can be added to package.json scripts:

"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'"
}

Running npm test now shows coverage for both the main and child processes.

(Unless otherwise noted, this article is licensed under CC BY‑NC‑ND 4.0.)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

code coveragetestingNode.jsmulti-processIstanbulmocha
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.