Integration Testing Interactive CLIs
How to test your interactive Node-based Command Line Interface using Jest and mock-stdout

I’ve been spending a lot of time on Gluegun, our Node-based CLI toolkit. Along the way, I’ve studiously avoided building in-depth integration tests, because I didn’t know how to simulate user input. I knew I would eventually need to bite the bullet and learn. So this week I took a deep breath, made myself a hot cup of coffee, and dove in.
Gluegun CLI
I decided to build integration tests for Gluegun’s “prompt” extension. Gluegun’s CLI allows you to generate a new CLI, powered by Gluegun itself. When spinning up a new CLI, you can choose whether you want to use TypeScript or JavaScript.

This uses the very cool library Enquirer to display these prompts. Enquirer is a built-in dependency of Gluegun. (Not to be confused with its much larger predecessor, Inquirer!)
NOTE: For more about Gluegun, see the links at the end of this article.
Mocking Standard Input
What we had done in our tests in the past was to pass in a flag, --typescript
, to tell it if we wanted a TypeScript CLI or not. But this only tested that the flag worked, not if the user interface did.
I needed a way to mock “stdin” (standard input) in the terminal and send keystrokes — specifically, “down arrow” and “enter” being the most important.
In order to set up the test, I first added the NPM module mock-stdin.
$ yarn add -D mock-stdin
I then imported it at the top of my test file, like so:
import { stdin } from 'mock-stdin'
(Note that all of my code examples will be in modern JavaScript.)
Since I’m using Jest to run my tests, I set up a beforeAll
and afterAll
hook to ensure I’m only mocking stdin
while the tests are running. If I don’t do that, it could have unintended side effects.
// Mock stdin so we can send messages to the CLIlet io = null
beforeAll(() => (io = stdin()))
afterAll(() => io.restore())
I can then “send” keystrokes using io.send(...)
.
Integration Test
Now I’m ready to make our integration test. I can load up the prompt
library directly from Gluegun and use it programmatically.
import { prompt } from 'gluegun/prompt'
I then want to run the Jest tests in asynchronous mode, since I’ll be sending keystrokes and then waiting to see what happens.
test('prompt for input', async done => {
// tests here
done()
})
NOTE: The done()
call tells Jest that the test is done running, since it wouldn’t know otherwise in an asynchronous function.
One more thing before I can write our test. I need to know what the ASCII escape code is for the arrow keys, enter, and any other keystroke I want to send.
This took me a lot of experimentation — probably at least a half hour of Googling and trying things. But I’ll give you the answer right now:
// Key codes
const keys = {
up: '\x1B\x5B\x41',
down: '\x1B\x5B\x42',
enter: '\x0D',
space: '\x20'
}
Okay, cool, now comes the fun part! Let’s write a test.
First, I’ll trigger the prompt:
test('prompt for input', async done => {
const result = await prompt.ask({
name: 'shoe',
type: 'list',
description: 'What shoes are you wearing?',
choices: [ 'Suede', 'Rubber Boots', 'High Heels' ]
}) done()
})
Next, I will write the test:
test('prompt for input', async done => {
const result = await prompt.ask({
name: 'shoe',
type: 'list',
description: 'What shoes are you wearing?',
choices: [ 'Suede', 'Rubber Boots', 'High Heels' ]
}) expect(result).toEqual({ shoe: 'Rubber Boots' }) done()
})
And lastly, I need to make an async function that I can use to send keystrokes. While I’m not doing anything async right now, I will after a bit, so I might as well plan ahead. I’ll also set a 5ms timer to fire off the keystrokes after the prompt is displayed.
test('prompt for input', async done => {
const sendKeystrokes = async () => {
io.send(keys.down)
io.send(keys.down)
io.send(keys.enter)
}
setTimeout(() => sendKeystrokes().then(), 5) const result = await prompt.ask({
name: 'shoe',
type: 'list',
description: 'What shoes are you wearing?',
choices: [ 'Suede', 'Rubber Boots', 'High Heels' ]
}) expect(result).toEqual({ shoe: 'Rubber Boots' }) done()
})
Here’s the end result. When I run the test, it passes!
import { stdin } from 'mock-stdin'
import { prompt } from 'gluegun/prompt'// Key codes
const keys = {
up: '\x1B\x5B\x41',
down: '\x1B\x5B\x42',
enter: '\x0D',
space: '\x20'
}// Mock stdin so we can send messages to the CLI
let io = null
beforeAll(() => (io = stdin()))
afterAll(() => io.restore())test('prompt for input', async done => {
const sendKeystrokes = async () => {
io.send(keys.down)
io.send(keys.down)
io.send(keys.enter)
}
setTimeout(() => sendKeystrokes().then(), 5) const result = await prompt.ask({
name: 'shoe',
type: 'list',
description: 'What shoes are you wearing?',
choices: [ 'Suede', 'Rubber Boots', 'High Heels' ]
}) expect(result).toEqual({ shoe: 'Rubber Boots' }) done()
})
Multiple Inputs
I’d like to ask several questions and test them all. When I was debugging this, I noticed that I needed to wait about 10ms between prompts to ensure the next one was displayed before firing off the keystrokes — otherwise the test would fail.
To do this, I made a little async delay
helper function.
// helper function for timing
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
I can use it by writing await delay(10)
. Easy peasy.
Let’s look at how the multiple inputs looks:
import { stdin } from 'mock-stdin'
import { prompt } from 'gluegun/prompt'// Key codes
const keys = {
up: '\x1B\x5B\x41',
down: '\x1B\x5B\x42',
enter: '\x0D',
space: '\x20'
}// helper function for timing
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))// Mock stdin so we can send messages to the CLI
let io = null
beforeAll(() => (io = stdin()))
afterAll(() => io.restore())test('prompt for input', async done => {
const sendKeystrokes = async () => {
// shoes
io.send(keys.down)
io.send(keys.down)
io.send(keys.enter)
await delay(10) // name
io.send('Jamon')
io.send(keys.enter)
await delay(10) // hats
io.send(keys.down)
io.send(keys.space)
io.send(keys.down)
io.send(keys.space)
io.send(keys.enter)
}
setTimeout(() => sendKeystrokes().then(), 5) const result = await prompt.ask([{
name: 'shoe',
type: 'list',
description: 'What shoes are you wearing?',
choices: [ 'Suede', 'Rubber Boots', 'High Heels' ]
}, {
name: 'name',
type: 'input',
description: 'What is your name?'
}, {
name: 'hats',
type: 'radio',
description: 'What hats do you own?',
choices: [ 'Bowler', 'Fedora', 'Boater' ]
}]) expect(result).toEqual({
shoe: 'Rubber Boots',
name: 'Jamon',
hats: [ 'Fedora', 'Boater' ]
}) done()
})
And we’re done!
Actual Implementation
While I simplified the implementation a bit for the purposes of this blog post, you can check out the actual implementation in this pull request:
Other Links
- If you have questions about this, or if it helped you, please let me know on Twitter! https://twitter.com/jamonholmgren
- If you want to learn more about Gluegun, check out this blog post I wrote recently: Announcing Gluegun 2.0: A delightful way to build command line apps in Node
- If you’re looking for software design and development, especially in React Native, React, Node, and other JavaScript-centric technologies, check out my company Infinite Red
