This has been brought up before and rejected. The referenced link there is dead, so maybe I'm missing something, but I still find prompts a little difficult to test.
Specifically, I want to be able to easily test if questions are skipped, if certain answers are considered invalid, how the messages, defaults, etc, are impacted by state.
I would like to see some sort of API like this:
const mockInquirer = require('inquirer/mock');
const promptState = mockInquirer([ ... prompts]);
const firstQuestion = await promptState.currentQuestion;
assert(firstQuestion.name === 'foo');
assert(await firstQuestion.message === 'foo message');
assert(await firstQuestion.default === 'foo default');
await firstQuestion.answer('bad answer').isInvalid('expected invalid message');
await prompts.answer('good answer').isValid();
const secondQuestion = await promptState.currentQuestion;
// make similar assertions as to the first
assert(promptState.skipped.length === 0);
await secondQuestion.answer('some answer that causes stuff to be skipped');
assert(promptState.skipped.length === 1);
assert(promptState.skipped[0].name === 'skipped question');
I agree. A better way to unit test would be ideal. Writing unit tests for prompts is difficult, and providing a clear testing API would be amazing. I like many of the API suggestions by @jamestalmage.
馃憤
馃憤
馃憤
:+1:
馃憤
馃憤
Probably someone should start working on this? Being a contributor is hard in nature, so external help is gold! Hint hint!
Use Jest 馃挴 Ref
Note : I discovered that inquirer default field could receive a function after coding all this mess... Damn me.
If anyone is interested, here is how I tested code using inquirer with sinon.js.
const askCredentials = () => {
const credentials = {};
return inquirer.prompt(inquiries.dbms)
.then((onFulfilled) => {
credentials.dbms = onFulfilled.dbms;
inquiries.port.default = cst.dbmsList[credentials.dbms].defaultPort;
return inquirer.prompt([inquiries.host, inquiries.port, inquiries.user, inquiries.password]);
})
.then(onFulfilled => Object.assign(credentials, onFulfilled));
};
This function prompts the user required information to log in a database. The object inquiries contains the questions inquirer must ask. It first asks the user what dbms he uses and then set the corresponding default port for the following question.
And here is how I test this function :
const assert = require('assert');
const sinon = require('sinon');
const inquirer = require('inquirer');
const prompt = require('../prompt'); // contains the method I'm testing
const cst = require('../constants');
const sandbox = sinon.sandbox.create();
const inq = cst.inquiries;
describe('askCredentials', function () {
afterEach(function () {
sandbox.restore();
});
it('provides the correct default for the chosen dbms', function () {
const inquirerPromptStub = sandbox.stub(inquirer, 'prompt').onCall(0).resolves({ dbms: cst.dbmsList.mysql.name });
inquirerPromptStub.onCall(1).resolves(null);
const expectedPortInq = Object.assign({ default: 3306 }, inq.port);
return prompt.askCredentials()
.then((onFulfilled) => {
assert.deepEqual(inquirerPromptStub.getCall(1).args[0], [inq.host, expectedPortInq, inq.user, inq.password]);
});
});
});
So the idea is... not to use inquirer during the tests. I replaced the function inquirer.prompt with one function I defined and which will simply return what I want it to return.
I wrote an helper to be used inside tests, it should support all the prompt.types available.
@SBoudrias any plan to integrate mocking tools directly inside Inquirer or should we build an external package to do it?
Example of usage:
'use strict';
import test from 'ava';
import inquirer from 'inquirer';
import mocki from './inquirer-mock-prompt.js';
// The mock is automatically removed after the answers are returned.
test('work should be false', async t => {
mocki({
work: false
});
const answers = await inquirer.prompt({
type: 'confirm',
name: 'work',
default: true
});
t.is(answers.work, false); // => true
});
// You can even mock multiple times
test('work should be false', async t => {
mocki({
work: false
});
mocki({
text: 'cool'
});
let answers1 = await inquirer.prompt({
type: 'confirm',
name: 'work',
default: true
});
t.is(answers.work, false); // => true
answers = await inquirer.prompt({
type: 'input',
name: 'text',
});
t.is(answers.text, 'cool'); // => true
});
Content of inquirer-mock-prompt.js
/* eslint-disable no-await-in-loop */
'use strict';
const inquirer = require('inquirer');
const isNumber = i => typeof i === 'number';
const isFunction = i => typeof i === 'function';
const isUndefined = i => typeof i === 'undefined';
/**
* @param {Object} prompt
* @param {Object} answers
* @param {string} input
* @return {Promise.<string|string[]|Object>}
*/
async function promptHandler(prompt, answers, input) {
if (prompt.when === false) {
return;
}
if (isFunction(prompt.when) && !await prompt.when(answers)) {
return;
}
if (isFunction(prompt.message)) {
// Just for coverage
prompt.message(answers);
}
if (isFunction(prompt.transformer)) {
// Just for coverage
prompt.message(input);
}
let answer = input;
if (isUndefined(answer)) {
if (isFunction(prompt.default)) {
answer = await prompt.default(answers);
} else {
answer = prompt.default;
}
if (isNumber(answer) && prompt.type in ['list', 'rawlist', 'expand']) {
if (isFunction(prompt.choiches)) {
answer = await prompt.choiches(answers)[answer];
} else {
answer = prompt.choiches[answer];
}
}
}
if (isUndefined(answer)) {
switch (prompt.type) {
case 'expand':
answer = {
key: 'h',
name: 'Help, list all options',
value: 'help',
};
break;
case 'checkbox':
answer = [];
break;
case 'confirm':
answer = false;
break;
default:
if (Array.isArray(prompt.choiches)) {
[answer] = prompt.choiches;
} else if (isFunction(prompt.choiches)) {
[answer] = await prompt.choiches(answers);
} else {
answer = '';
}
}
}
if (isFunction(prompt.filter)) {
answer = await prompt.filter(answer);
}
if (isFunction(prompt.validate)) {
const valid = await prompt.validate(answer, answers);
if (valid !== true) {
throw new Error(valid);
}
}
return answer;
}
/**
* @param {Object} inputs
* @return {Function}
*/
function inquirerHandler(inputs) {
/**
* @param {Object} prompts
* @return {Promise.<Object>}
*/
return async prompts => {
const answers = {};
for (const prompt of [].concat(prompts)) {
answers[prompt.name] = await promptHandler(
prompt,
answers,
inputs[prompt.name]
);
}
return answers;
};
}
/**
* @param {Object|Object[]} inputs
*/
function mock(inputs) {
if (typeof inputs !== 'object') {
throw new TypeError('The mocked answers must be an objects.');
}
const promptOriginal = inquirer.prompt;
const promptMock = async function(questions) {
try {
const answers = await inquirerHandler(inputs)(questions);
inquirer.prompt = promptOriginal;
return Promise.resolve(answers);
} catch (err) {
inquirer.prompt = promptOriginal;
return Promise.reject(err);
}
};
promptMock.prompts = inquirer.prompt.prompts;
promptMock.registerPrompt = inquirer.prompt.registerPrompt;
promptMock.restoreDefaultPrompts = inquirer.prompt.restoreDefaultPrompts;
inquirer.prompt = promptMock;
}
module.exports = mock;
@simonepri I'm not planning to do that right now, but I'm happy to add documentation to the wiki or the readme - and if the community adopt a solution, then I think we could consider bringing this into the core library.
As you can see, I've tried to re-implement the way how inquirer threat the prompts object.
defaults -> filter -> validate
This seems really a lot of unnecessary effort on our side.
Also because maybe my implementation is just wrong.
No way to export the methods that inquirer uses internally to handle the answers after the input has been read from stdin?
This would make everything simpler.
@simonepri, @SBoudrias Has there been any update or development on this? Running into this issue on a project of mine now. I'd be happy to provide support along the way.
I'm happy to merge test utilities as a test new package to the monorepo. Maybe like @inquirer/test.
I'm not planning to work on this myself, but I'm happy to help with reviewing and merging the initial version.
Is there a way to write unit test cases for such cases?
I don't know how to verify the validate: (value) branch?
I'm using sinon.js fyi
inquirer.prompt([{
message : 'test',
type : 'input',
validate : (value) => {
if(value === null) return 'Error';
if( value.length == 3) return 'Error';
return true;
}
}]).then(result) ........
I'd just like to clarify as I didn't in this ticket. I'm actually not personally too interested in this concept as I do write UT for project using inquirer and a ton of other libs without issues by mocking inquirer (jest.mock('inquirer')).
I usually don't need any smart behavior applied for me, just need an answer returned to test it worked fine. And if need, I manually call the filter/validate/etc methods to tests they're working as expected.
I'm still happy to help review and merge a proposal - maybe as part of the refactor #692. But I hope this clarification can help most readers who might think inquirer isn't testable - you never need a library to provide test tool, just a mock library (jest, proxyquire, mockery, etc)
How would suggest recommend to test code that use the reactive interface to dynamically add questions?
For example how would you test this example that asks for a confirmation and make a backend call based on previous answers:
const inquirer = require('inquirer');
const {Subject} = require('rxjs');
async function checkOnServer({quantity}) {
return quantity <= 20;
}
const quantity = {
type: 'input',
name: 'quantity',
message: 'How many do you need?',
filter: Number
};
const confirm = {
type: 'confirm',
name: 'confirm',
message: 'That\'s a lot, are you sure? A server verification will be performed.'
};
var prompts = new Subject();
const prompt = inquirer.prompt(prompts);
const answers = {};
prompt.ui.process.subscribe(async ({name, answer}) => {
Object.assign(answers, {[name]: answer});
if (name === 'quantity' && answer >= 10) {
return prompts.next(confirm);
} else if (name === 'confirm' && !answer) {
return prompts.complete();
} else if (name === 'confirm' && answer) {
answers.valid = await checkOnServer(answers);
return prompts.complete();
} else {
return prompts.complete();
}
},
() => {},
() => {
if (answers.confirm === false) {
console.log('No quantity selected');
} else if (answers.valid === false) {
console.log('Server authorization failed');
} else {
console.log(`Quantity: ${answers.quantity}`);
}
});
prompts.next(quantity);
Thanks for your help!
https://gist.github.com/yyx990803/f61f347b6892078c40a9e8e77b9bd984
This is a nice mock for integration testing prompts.
Most helpful comment
I wrote an helper to be used inside tests, it should support all the
prompt.typesavailable.@SBoudrias any plan to integrate mocking tools directly inside Inquirer or should we build an external package to do it?
Example of usage:
Content of
inquirer-mock-prompt.js