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 | console.log('are you happy?'); |
$ 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 | var Promise = require('bluebird'); |
$ 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
1 | var Promise = require('bluebird'); |
We use ask
just like before
1 | var ask = require('./ask'); |
step 2 - setup unit test
I will pick Mocha unit testing framework due to its good promise support.
1 | var ask = require('./ask'); |
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.
1 | var ask = require('./ask'); |
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.
1 | var ask = require('./ask'); |
One can even provide multiple answers to questions asked in order.
1 | var ask = require('./ask'); |
Or you can simply provide the expected input before each question
1 | it('asks three questions separately', function () { |
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 | var inquirer = require('inquirer'); |
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.
1 | bddStdin.keys = { |
In our unit tests we can simply supply the down key as many times as necessary, followed by the newline character.
1 | var bddStdin = require('bdd-stdin'); |
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
1 | "scripts": { |
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
1 | it('selects the third choice - nogit', function () { |
Then I modify the commit hook command to skip these specs
1 | "pre-commit": [ |
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 | var bddStdin = require('..'); |
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.
1 | require = require('really-need'); |
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
1 | var inquirer; |
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
1 | var inquirer; |
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!
1 | require = require('really-need'); |
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 | function emitSpace() { |
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
- blog post Mock system APIs
- My collection of mocking utilities and examples for Node CLI program bahmutov/node-mock-examples