如何使用 mocha、chai 和 robotjs 测试需要用户在命令行中输入的功能?

How to test function that requires user input in command line using mocha, chai, and robotjs?

我正在尝试使用 mocha、chai 和 robotjs 对该函数进行单元测试。我目前可以让 robotjs 输入数据,但我无法自行测试此功能。现在我的终端显示我希望 {} 等于 'https://toscrape.com/'。 'https://toscrape.com/' 是这个函数中使用的baseurl,如果用户输入no,应该返回这个。稍后从另一个函数返回一个对象。如何测试此功能?

// allow user to input a url and then validate url
const requestSiteURL = async function () {
  let url = await new Promise((resolve) => {
    readline.question('Please type url: ', resolve);
  });
  let URL = 'https://toscrape.com/';
  if (validUrl.isUri(url)) {
    readline.close();
    return url;
  } else if (url.toLowerCase() === 'no') {
    url = URL;
    return url;
  } else {
    console.log(
      'Please type in a valid URL (https://toscrape.com/) or type "no" to use base url.'
    );
    return requestSiteURL();
  }
};

这是我到目前为止的测试用例


const url = require('../crawler');
const robot = require('robotjs');
var chai = require('chai');
var expect = chai.expect;

var roboInput = () => {
  robot.typeString('no');
  robot.keyTap('enter');
};

describe('validates site url', function () {
  it('no', function () {
    roboInput();
    let result = url.requestSiteURL();
    expect(result).to.equal('https://toscrape.com/');
  });
});

您想编写单元测试,因此您需要在一个隔离的、无副作用的环境中测试您的代码。为了提供这样的环境,有必要为具有副作用的模块和方法创建存根或模拟。这里我将使用sinon.js,因为它经常与mocha测试框架和chai断言库一起使用。

什么是隔离环境?对于您的示例,我们不需要用户或 robotjs 自动化脚本来真正在终端中输入文本。

对于您的情况,我们应该为 readline.question()readline.close() 等创建存根。

例如

crawler.js:

const rl = require('readline');

const validUrl = {
  isUri(url) {
    return url === 'https://toscrape.com/';
  },
};
const readline = rl.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const requestSiteURL = async function() {
  let url = await new Promise((resolve) => {
    readline.question('Please type url: ', resolve);
  });
  let URL = 'https://toscrape.com/';
  if (validUrl.isUri(url)) {
    readline.close();
    return url;
  } else if (url.toLowerCase() === 'no') {
    url = URL;
    return url;
  } else {
    console.log('Please type in a valid URL (https://toscrape.com/) or type "no" to use base url.');
    return requestSiteURL();
  }
};

module.exports = { requestSiteURL };

crawler.test.js:

const chai = require('chai');
const sinon = require('sinon');
const readline = require('readline');
const expect = chai.expect;

function resetModules() {
  delete require.cache[require.resolve('./crawler')];
}

describe('65298539', () => {
  beforeEach(() => {
    resetModules();
  });
  afterEach(() => {
    sinon.restore();
  });
  it('should return url if it is valid', async () => {
    const readlineInterfaceStub = {
      question: sinon.stub().callsFake((query, callback) => {
        callback('https://toscrape.com/');
      }),
      close: sinon.stub(),
    };
    sinon.stub(readline, 'createInterface').returns(readlineInterfaceStub);
    const url = require('./crawler');
    const actual = await url.requestSiteURL();
    expect(actual).to.be.eql('https://toscrape.com/');
    sinon.assert.calledWithExactly(readlineInterfaceStub.question, 'Please type url: ', sinon.match.func);
    sinon.assert.calledOnce(readlineInterfaceStub.close);
  });

  it('should set default url if user enter "no"', async () => {
    const readlineInterfaceStub = {
      question: sinon.stub().callsFake((query, callback) => {
        callback('No');
      }),
    };
    sinon.stub(readline, 'createInterface').returns(readlineInterfaceStub);
    const url = require('./crawler');
    const actual = await url.requestSiteURL();
    expect(actual).to.be.eql('https://toscrape.com/');
    sinon.assert.calledWithExactly(readlineInterfaceStub.question, 'Please type url: ', sinon.match.func);
  });

  it('should recursive call', async () => {
    let callCount = 0;
    const readlineInterfaceStub = {
      question: sinon.stub().callsFake((query, callback) => {
        if (callCount === 0) {
          callCount++;
          callback('');
        } else {
          callback('No');
        }
      }),
    };
    sinon.stub(readline, 'createInterface').returns(readlineInterfaceStub);
    sinon.spy(console, 'log');

    const url = require('./crawler');
    const actual = await url.requestSiteURL();
    expect(actual).to.be.eql('https://toscrape.com/');
    sinon.assert.calledWithExactly(readlineInterfaceStub.question, 'Please type url: ', sinon.match.func);
    sinon.assert.calledWithExactly(
      console.log,
      'Please type in a valid URL (https://toscrape.com/) or type "no" to use base url.',
    );
  });
});

单元测试结果:

  65298539
    ✓ should return url if it is valid (1516ms)
    ✓ should set default url if user enter "no"
Please type in a valid URL (https://toscrape.com/) or type "no" to use base url.
    ✓ should recursive call


  3 passing (2s)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |     100 |      100 |     100 |     100 |                   
 crawler.js |     100 |      100 |     100 |     100 |                   
------------|---------|----------|---------|---------|-------------------