Unit testing CLI programs

Writing mock stdin text in your Nodejs unit tests.

Imagine you need to unit test code that asks user a question. How would you do this in Nodejs? Asking a user a question and reading an answer is pretty simple:

1
2
3
4
5
console.log('are you happy?');
process.stdin.once('data', function (data) {
console.log('user replied', data.toString().trim());
process.exit();
});
$ node index.js
are you happy?
yes
user replied yes

It is convenient to move this question / response functionality into a promise-returning function, similar to inquirer-confirm.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Promise = require('bluebird');
function ask(question) {
console.log(question);
return new Promise(function (resolve) {
process.stdin.once('data', function (data) {
resolve(data.toString().trim());
});
});
}
ask('are you happy?')
.then(function (reply) {
console.log('user replied', reply);
})
.finally(process.exit);
$ node index.js
are you happy?
yes
user replied yes

How can we unit test the function ask?

step 1 - refactor

The first step we always should do when unit testing any piece of code is to refactor it to be nicely separated from the rest of the code. In this case we move the question to its own file

ask.js
1
2
3
4
5
6
7
8
9
10
var Promise = require('bluebird');
function ask(question) {
console.log(question);
return new Promise(function (resolve) {
process.stdin.once('data', function (data) {
resolve(data.toString().trim());
});
});
}
module.exports = ask;

We use ask just like before

index.js
1
2
3
4
5
6
var ask = require('./ask');
ask('are you happy?')
.then(function (reply) {
console.log('user replied', reply);
})
.finally(process.exit);

step 2 - setup unit test

I will pick Mocha unit testing framework due to its good promise support.

ask-spec.js
1
2
3
4
5
6
7
8
9
var ask = require('./ask');
describe('ask', function () {
it('asks a question', function () {
return ask('test')
.then(function () {
// ?
});
});
});

Right now the test times out because it does not provide any input to the process.stdin stream

$ mocha ask-spec.js
  ask
test
    1) asks a question
  0 passing (2s)
  1 failing
  1) ask asks a question:
     Error: timeout of 2000ms exceeded

step 3 - provide test answer

We can simply feed a string into the process.stdin in our unit test. A good technique is replacing actual process.stdin with a mock stream object. I use moch-stdin package that allows this.

ask-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ask = require('./ask');
describe('ask', function () {
var stdin;
beforeEach(function () {
stdin = require('mock-stdin').stdin();
});
it('asks a question', function () {
process.nextTick(function mockResponse() {
stdin.send('response');
});
return ask('question: test')
.then(function (response) {
console.assert(response === 'response');
});
});
});

Notice the delayed function mockResponse running inside the unit test. The delay is necessary to properly order the test steps

ask the question and wait for response
send 'response' to the mock stdin
read the data from the mock stdin
execute the next step in the promise chain

Without the async / delayed mockResponse function, it would have provided the data to the mock stdin before it was ready to read the data inside the ask function.

bdd-stdin

To simplify the testing logic I have written bdd-stdin that hides the extra details and can feed multiple answers in turns.

using bdd-stdin
1
2
3
4
5
6
7
8
9
10
11
var ask = require('./ask');
var bddStdin = require('bdd-stdin');
describe('ask', function () {
it('asks one question', function () {
bddStdin('answer');
return ask('type "answer"')
.then(function (response) {
console.assert(response === 'answer');
});
});
});

One can even provide multiple answers to questions asked in order.

multiple mock responses
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ask = require('./ask');
var bddStdin = require('bdd-stdin');
describe('ask', function () {
it('asks one question', function () {
bddStdin('one', 'two');
return ask('type "one"')
.then(function (response) {
console.assert(response === 'one');
return ask('type "two"');
})
.then(function (response) {
console.assert(response === 'two');
});
});
});

Or you can simply provide the expected input before each question

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it('asks three questions separately', function () {
bddStdin('one');
return ask('question 1')
.then(function (response) {
console.assert(response === 'one');
bddStdin('two');
return ask('question 2');
})
.then(function (response) {
console.assert(response === 'two');
bddStdin('three');
return ask('question 3');
}).then(function (response) {
console.assert(response === 'three');
});
});

bdd-stdin currently is pretty simple program and might not work with complicated async logic.

picking a choice from a list

Prompt utilities like inquirer have an option of picking a choice from a list of words. The user has to use keyboard UP / DOWN keys to highlight the desired line, then press ENTER to confirm the selection.

1
2
3
4
5
6
7
8
9
10
var inquirer = require('inquirer');
var question = {
type: 'list',
name: 'choice',
message: 'pick three',
choices: ['one', 'two', 'three']
};
inquirer.prompt([question], function (answers) {
console.log('user picked', answers.choice);
});

presents the user with the following prompt:

? pick three: (Use arrow keys)
❯ one
  two
  three

How can we simulate clicking arrow buttons and entering the desired choice in our bdd-stdin utility? Pretty simple. We can enter the desired key codes. To simplify using bdd-stdin I added a few keyboard codes to its exported function.

bdd-stdin.js
1
2
3
4
5
6
7
bddStdin.keys = {
up: '\u001b[A',
down: '\u001b[B',
left: '\u001b[D',
right: '\u001b[C'
};
module.exports = bddStdin;

In our unit tests we can simply supply the down key as many times as necessary, followed by the newline character.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var bddStdin = require('bdd-stdin');
it('selects the third choice', function (done) {
bddStdin(bddStdin.keys.down, bddStdin.keys.down, '\n');
var question = {
type: 'list',
name: 'choice',
message: 'pick three',
choices: ['one', 'two', 'three']
};
inquirer.prompt([question], function (answers) {
console.assert(response === 'three');
done();
});
});

running choice spec on git commit

I try to never break the master branch. To achieve this I run the unit tests automatically on git commit command using pre-git package.

npm install --save-dev pre-git

By default I run the npm test and npm version commands on each commit

package.json
1
2
3
4
5
6
7
"scripts": {
"test": "mocha test/*-spec.js"
},
"pre-commit": [
"npm test",
"npm version"
]

This pre-commit hook does NOT work well with feeding the synthetic input to the standard input stream during unit tests. The individual text is entered fine, but the control characters, like UP and DOWN keys are not sent correctly. I need to avoid running just these unit tests on commit. Luckily, my favorite JavaScript unit testing framework is Mocha; it handles this situation beautifully.

I add a keyword to the unit tests to be avoided when running the test command on the git commit step. For example it could be the nogit word

spec.js
1
2
3
4
5
6
7
it('selects the third choice - nogit', function () {
bddStdin(bddStdin.keys.down, bddStdin.keys.down, '\n');
return choice('pick three', ['one', 'two', 'three'])
.then(function (response) {
console.assert(response === 'three', 'received response ' + response);
});
});

Then I modify the commit hook command to skip these specs

package.json
1
2
3
4
"pre-commit": [
"npm test -- --grep nogit --invert",
"npm version"
]

The -- tells NPM to pass all the following options to the mocha command. The --grep option selects the specific specs (by name), while --invert option means the selected specs will be skipped. The original npm test command still runs all specs without skipping the ones testing the choice feature.

advanced: stubbing the prompt method using sinon.js

Let us stub the inquirer.prompt method using Sinon.js library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var bddStdin = require('..');
var sinon = require('sinon');
var inquirer = require('inquirer');
describe('mock prompt', function () {
var question = {
type: 'checkbox',
name: 'all',
message: 'pick options',
choices: ['one', 'two', 'three']
};
beforeEach(function () {
sinon.stub(inquirer, 'prompt', function (questions, cb) {
setTimeout(function () {
cb({
all: ['one']
});
}, 0);
});
});
afterEach(function () {
inquirer.prompt.restore();
});
it('select first choice', function (done) {
inquirer.prompt([question], function (answers) {
var response = answers.all;
console.assert(Array.isArray(response) &&
response.length === 1 &&
response[0] === 'one',
'received wrong choices ' + response);
done();
});
});
});

We always return mock answer from the inquire.prompt stub, and we restore the original method after each unit test. If you need more details how to use Sinon.js library, read Spying on methods.

advanced: mocking the prompt method directly

The final way of entering the fake user input during unit tests works around the entire problem. Instead of mocking the input stream, we will mock any method asking for the user's input. We can easily do this inside the specs using a replacement for Node's built-in require method I wrote and called really-need. One of the options it exposes during require call is an ability to transform the exported true inquirer into our method.

spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require = require('really-need');
// require is now more powerful
describe('mock prompt', function () {
var question = {
type: 'checkbox',
name: 'all',
message: 'pick options',
choices: ['one', 'two', 'three']
};
var inquirer;
beforeEach(function () {
inquirer = require('inquirer', {
// make sure to remove any previously loaded instance
bust: true,
post: function post(inq) {
// transform / mock inquirer.prompt method
return inq;
}
});
});
it('select the first choice only', function (done) {
inquirer.prompt([question], function (answers) {
var response = answers.all;
console.assert(Array.isArray(response) &&
response.length === 1 &&
response[0] === 'one',
'received wrong choices ' + response);
done();
});
});
});

Inside the post callback we can wrap the inquirer.prompt and do anything we want with it. For this spec I will return the same answer right away

spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var inquirer;
beforeEach(function () {
inquirer = require('inquirer', {
// make sure to remove any previously loaded instance
bust: true,
post: function post(inq) {
inq.prompt = function (questions, cb) {
cb({
all: ['one']
});
};
return inq;
}
});
});

While in this simple case it works, the semantics of the mocked inq.prompt is incorrect. The method has to call its callback cb asynchronously, not right away. We can easily defer the call

spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var inquirer;
beforeEach(function () {
inquirer = require('inquirer', {
// make sure to remove any previously loaded instance
bust: true,
post: function post(inq) {
inq.prompt = function (questions, cb) {
setTimeout(function () {
cb({
all: ['one']
});
}, 0);
};
return inq;
}
});
});

Now the callback will be called via event loop schedule, respecting the async behavior. For full example code see the spec file.

really-need vs method stubs

Why do we need to go into the trouble replacing require with really-need to achieve the same feature provided by Sinon.js? Replacing a module during the require step is very powerful and can replace a module loaded indirectly. For example, inquirer uses module readline2 to actually control the input / output streams. Using cache bustin provided by the really-need we can mock readline2 and the inquirer module just uses the mocked version!

the spec setup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
require = require('really-need');
describe('mock readline', function () {
var inquirer;
mockRl = {
pause: function () {},
resume: function () {},
close: function () {},
on: function () {},
removeListener: function () {},
output: {
mute: function () {},
unmute: function () {},
end: function () {},
write: function () {}
},
addEventListener: function (name, handler) {
if (!mockRl._listeners[name]) {
mockRl._listeners[name] = [];
}
mockRl._listeners[name].push(handler);
},
removeEventListener: function () {},
setPrompt: function () {},
_listeners: {}
};
beforeEach(function () {
require('inquirer/node_modules/readline2', {
bust: true,
keep: true,
post: function (rl2) {
rl2.createInterface = function () {
return mockRl;
};
return rl2;
}
});
inquirer = require('inquirer', {
bust: true
});
});
});

Notice how we replaced readline2.createInterface method with our own implementation that returns very empty mockRl object. The mockRl is almost useless, but it collects event listeners (from inquirer) and can send them synthetic key events without going through the actual input stream!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function emitSpace() {
setTimeout(function () {
if (mockRl._listeners.keypress) {
mockRl._listeners.keypress.forEach(function (handler) {
// send keypress 'space'
handler(undefined, { name: 'space' });
});
}
}, 0);
}
function emitNewLine() {
setTimeout(function () {
if (mockRl._listeners.line) {
// just need empty event callback execution
mockRl._listeners.line.forEach(handler);
}
}, 0);
}
it('select first choice', function (done) {
emitSpace();
emitNewLine();
var question = {
type: 'checkbox',
name: 'all',
message: 'pick options',
choices: ['one', 'two', 'three']
};
inquirer.prompt([question], function (answers) {
var response = answers.all;
console.assert(Array.isArray(response) &&
response.length === 1 &&
response[0] === 'one',
'received wrong choices ' + response);
done();
});
});

We just sent synthetic events from our mock readline2 object to the reactive streams used by the inquirer This is an advanced technique and is an overkill in this case, but sometimes it might be necessary if the mocked feature is far away from the tested api.

See also