⚙️ Integration Tests on Node.js CLI: Part 3 – Inter Process Communication

Andrés Zorro
6 min readDec 3, 2018

This article is part of a series about writing Node.js CLI programs and more specifically, testing them by writing E2E/Integration tests, rather than writing the usual module unit test. If you want to skip to the final implementation code, check here. The links to the other parts are listed below:

If you have been working on CLI testing for some time, or have been following up this series, you should be aware by now of the unique pickle we are into: how to test a process from within another process? We have provided some solutions for dealing with input/output, and we have successfully fed data into the process to provide and test expected results. In a real world scenario though, CLIs are meant to do processes, most of them asynchronous like fetching data from a server, storing data, sending update operations (CRUD) to an API endpoint, perform calculations, upload code and so many other uses. Some examples of this are CLIs like AWS-CLI, Heroku Toolbelt (now Heroku CLI), Git, Serverless, npm itself.

While developing the CLI project I was working on, I started thinking about the testing strategy rather early. So most of the tests — and the whole approach I wrote about in the previous entries — were written for one specific feature of the CLI that wasn’t using any service (it was doing I/O, but over local files). Imagine my surprise when I was about to apply all the lessons learned to the rest of the features… and they didn’t work. Well, they did, but they were pinging actual services. Probably not a good approach for our Continuous Integration setup. Or was it, maybe? I was writing E2E tests anyway. The thing was that I didn’t want to ping our real services.

With this idea in mind I started to look into how to mock services. Some techniques included module monkey patching and environment variables, but these methods didn’t give me enough flexibility to pass custom data for each test independently, not at least without much clutter in the original codebase— which wasn’t ideal anyway. So I started looking into ways to communicate with the parent process (the test runner) for it to send down information to the child process. Turns out that Node.js allows you to do exactly that. Under child_process.spawn API there’s an option for called Inter Process Communication (IPC). When a Node.js process is spawned with this option, global process.send and process.on methods are created in the child process, enabling an effective pub/sub pattern.

Ask your parents

In order to enable IPC, we need to add two small changes to our previous code. The first one is in executeWithInput method:

const concat = require('concat-stream');function executeWithInput(
processPath,
args = [],
inputs = [],
opts = {}
) {
// ...omitted for brevity...
const childProcess = createProcess(processPath, args, env); // ...more omissions... const promise = new Promise((resolve, reject) => { // ...almost there... childProcess.stdout.pipe(
concat(result => {
resolve(result.toString());
})
);
});
// Appending the process to the promise, in order to
// add additional parameters or behavior
// such as IPC communication

promise.attachedProcess = childProcess;
return promise;
}
module.exports = { execute: executeWithInput };

The second change is in the createProcess method:

const spawn = require('child_process').spawn;function createProcess(processPath, args = [], env = null) {
args = [processPath].concat(args);

return spawn('node', args, {
env: Object.assign(
{
NODE_ENV: 'test'
},
env
),

// Enable IPC in child process. This syntax is explained in
// Node.js documentation:
https://bit.ly/2zAA6vq
stdio: [null, null, null, 'ipc']
});
}

Only when I say so

I will dedicate the last entry of these series to explain the service mocking strategy in depth. But for the sake of keeping this entry focused on IPC, I will describe another problem I faced and how I solved using this approach. When I create a child process in the test runner, the child process executes everything that is supposed to, right when you invoke it. So in theory, this works:

// my_process.js
(async () => {
const response = await callSomeAsyncService();
console.log('Response is:', response);
})();
// test_runner.js
const cmd = require('./cmd'); // From our last entries
describe('Test my process', () => {
it('should print the correct output', async () => {
const response = await cmd.execute('my_process.js');
expect(response).to.equal(
'Response is: response from async service'
);
});
});

But what if you want to mock callSomeAsyncService() from the test suite? A naive approach would look like this:

// my_process.jsprocess.on('mock', data => {
callSomeAsyncService = Promise.resolve(data);
});
(async () => {
const response = await callSomeAsyncService();
console.log('Response is:', response);
})();
// test_runner.js
const cmd = require('./cmd');
const mockResponse = 'response from mock service';
describe('Test my process', () => {
it('should print the correct output', async () => {
const promise = cmd.execute('my_process.js');
const childProcess = promise.attachedProcess;
childProcess.send('mock', mockResponse);
const response = await promise; // Spoiler alert: This will fail
expect(response).to.equal(
'Response is: response from mock service'
);
});
});

The problem is that the moment you call cmd.execute to get the promise, the process already started to execute. This means that by the time the test runner hits childProcess.send the process has already called callSomeAsyncService, without giving us a chance to mock the response. How to solve this?

Telling process when to start

Again, IPC to the rescue. If you’re able to communicate with the child process, you can of course tell the process when to start. In this case, an environment variable might be helpful, to avoid interrupting the regular flow of the process. So a little refactor to my_process:

// my_process.jsprocess.on('mock', data => {
callSomeAsyncService = Promise.resolve(data);
});
async function init() {
const response = await callSomeAsyncService();
console.log('Response is:', response);
});
process.on('start', init);// Execute the process immediately if not in test env
if (process.env.NODE_ENV !== 'test') {
init();
};

The change in the test runner is quite simple, I bet you already figured it out:

// test_runner.js
const cmd = require('./cmd');
const mockResponse = 'response from mock service';
describe('Test my process', () => {
it('should print the correct output', async () => {
const promise = cmd.execute('my_process.js');
const childProcess = promise.attachedProcess;
childProcess.send('mock', mockResponse);

// Once we know the mock is in place,
// kick off the process

childProcess.send('start');
const response = await promise; expect(response).to.equal(
'Response is: response from mock service'
);
});
});

That’s it for this entry! Node.js IPC is a really powerful pattern and this is just one of the many uses it has. I recommend this great article that explains and dives deeper into IPC from another — in my opinion, more traditional — perspective.

In part 4 I will try to layout the final thoughts on mocking services and the way I used all of the gathered lessons to implement the E2E testing solution that we use to verify the CLI project integrity, the tool that has been the source of inspiration for this series. This time I’ll give it a shot at publishing without reviewer, mostly because this entry took long enough to write as it is, and I know some of you have waited for this one a lot (Sorry! 😅). Feel free to comment if you think there’s room for improvement, editorial-wise too. Thanks for reading!

--

--