测试Commander.js命令行应用程序

300 阅读11分钟

本教程包括。

  1. CLI应用程序如何工作
  2. 为CLI应用程序编写测试
  3. 使用CircleCI自动测试命令行应用程序

在生产中破坏变化是不方便的,而且修复的成本也很高。使用像git clone < some GitHub repository > ,在你的终端上执行的命令是一种常见的做法,被称为使用命令行。这种做法可以比使用GUI更快、更有效。在本教程中,我将引导你完成测试命令行应用程序的过程git ,解释为什么你需要命令行应用程序,并详细描述它们的工作原理。

先决条件

要完成本教程,需要以下项目。

我们的教程是与平台无关的,但使用CircleCI作为一个例子。如果你没有CircleCI账户,请**在这里注册一个免费账户。**

设置应用程序

为了帮助你遵循本教程,我创建了一个Github仓库供你克隆。你将需要在终端运行以下命令来安装项目的依赖。

git clone https://github.com/CIRCLECI-GWP/palatial-cakes-cli-app.git

cd palatial-cakes-cli-app

npm install

为什么使用CLI应用程序?

复杂的图形用户界面(GUI)带来的技术进步是惊人的,但我们不能否认,命令行应用程序(CLI)催生了软件革命。

那么,究竟什么是CLI 应用程序?命令行界面(CLI)应用程序是一个允许通过计算机终端输入文本命令的程序,然后它们会被主机解释为正在执行的程序。

让我们来看看为什么在某些情况下CLI应用程序比GUI应用程序更受欢迎。

  1. 了解CLI命令和带有这些命令的工具可以提高开发者的工作效率。使用命令行的自动化(重复性任务)比使用GUI可以更快、更有效。
  2. 与GUI应用程序相比,CLI应用程序对机器的内存和处理能力要求较低。
  3. 由于其基于文本的界面,CLI应用程序可以在低分辨率的显示器上完美运行,并被大量的操作系统所支持。

描述CLI应用程序如何工作

使用CLI应用程序需要将文本命令输入并由主机终端执行。一些CLI应用程序允许用户从提供的默认options ,而另一些则需要在提供的选项之外手动输入文本。

让我们来看看fetch 命令执行时的命令行界面结构和语法示例,git 命令行应用程序。

Git fetch CLI command

在这种情况下,Program 关键字是实际程序的名称,它通常是一个名词。command (动词)描述了程序做什么或我们指示程序做什么。Arguments 允许CLI在处理数值时接受这些数值。修改命令行为的参数的文件类型被称为option 。这些参数以连字符的形式输入。

我们的样本CLI应用程序如何工作的概述

在本教程中,我创建了一个实践CLI,它将帮助你学习如何测试命令行应用程序。这个CLI应用程序可以让你订购蛋糕,并创建一个所有可以订购的蛋糕的列表。

运行我们的CLI应用程序有两个不同的命令:一个用于订购蛋糕,一个用于获取可订购的蛋糕列表。

订购蛋糕

要进行命令行的蛋糕订购,请运行。

npm run order-cake

列出所有的蛋糕

要获得所有可用蛋糕的列表,请运行这个命令。

npm run list-cakes

下面是使用CLI订购和列出蛋糕的两种不同工作流程的说明。

Palatial cakes application flows

为我们的CLI应用程序编写测试

现在你已经看到了我们的CLI应用程序是如何工作的,最好再检查一下,增加更多的变化不会破坏代码。测试还可以帮助你深入了解潜在的容易出错的地方。要做到这一点,你将为现有的蛋糕应用逻辑添加测试。然后你将使用jestjs.io/来测试你的应用程序。

了解蛋糕的订购逻辑

这就是处理蛋糕订单的逻辑是如何实现的。

/** lib/order.js **/
const inQuirerOder = async () => {
  const answers = await inquirer.prompt(orderDetailsQuestions);
  return Object.keys(answers).map(
    (key, index) => `\n${index + 1}:${key} => ${answers[key]}`
  );
};

使用Inquirer.js'sprompt() 方法,你向用户提供了一个选项列表。对于每个问题,你使用其键来检索用户选择的答案。

根据Inquirer.js的文档,提示方法接受一个包含问题对象的数组和另一个参数:answer,一个包含先前回答问题的值的对象。默认情况下,答案对象是空的。

inquirer.prompt(questions, answers) -> promise

为了测试这个逻辑,我们将提供问题和预期的答案,而不是仅仅提供问题,这将通过CLI提示输入。这导致Inquirer避免提示答案。

因为这个方法返回一个承诺,我们可以使用[async-await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) 模式来获得响应对象。

接下来你将测试订购各种蛋糕,所以将相关的测试分组到一个测试套件中是一个好的做法。下面提供了使用describe() 方法对几个相关测试进行分组的语法,以及三个测试块,对你的蛋糕进行不同类型的测试。

/** Order.spec.js **/

describe('Order different types of cakes', () => {
 test('order cake: type A', async () => {
   ...
 });
 test('order cake: type B', async () => {
   ...
 });

 test('order cake: type C', async () => {
   ...
 });
});

开始使用断言

order.js 文件和文件顶部的inquirer 模块中导入一切。

/** __tests__/order.spec.js **/

const order = require("../lib/order");
const inquirer = require("inquirer");

现在你有了所需的模块,你可以在第一个测试块内快速创建一个蛋糕对象;这将是我们的答案。你将从导入的订单文件中得到你的问题。

/** __tests__/order.spec.js **/
cakeA = {
  Coating: "Butter",
  type: "Strawberry",
  "Cake Size": "Medium",
  Toppings: "Fruit",
};

第一个蛋糕对象包含key-value 对,键取自原始问题对象。其他两个测试块之间的唯一区别将是蛋糕对象的值和对象的名称。

如前所述,我们将为Inquirer提示方法提供各种问题和答案。

const order_cli = await inquirer.prompt(order.orderDetailsQuestions, cakeA);

返回的承诺是一个包含用户在使用CLI时可能选择的答案的对象。因此,我们可以断言这些模拟的响应。

expect(order_cli).toMatchObject(cakeA);

如果你想查看整个order.spec.js 测试文件,你可以在这里找到它。

就这样,你现在能够测试你的蛋糕订购命令的各种输入和输出了。

列出蛋糕

测试处理蛋糕渲染的逻辑将是简单的。你将提供一个蛋糕列表,然后根据你的CLI所呈现的蛋糕列表来断言它。

完整的实现。

/** __tests__/cakes-list.spec.js **/
const renderCakes = require("../lib/cakes-list");

const results = [
  "\n1 => Strawberry",
  "\n2 => Vanilla",
  "\n3 => Mint",
  "\n4 => White Chocolate",
  "\n5 => Black Forest",
  "\n6 => Red Velvet",
  "\n7 => Fruit Cake",
];

test("renders cakes list", () => {
  expect(renderCakes()).toEqual(results);
});

这个片段导入了cakes-list 模块,然后使用renderCakes 方法来断言蛋糕列表与之前在你的命令行应用程序中声明的列表相同。

设置错误处理

你可能想确保在你的CLI应用程序中检测和处理错误,这样你就可以在错误发生时修复错误。你将在这里写的测试将涵盖unknown optionscommands 、和undefined 选项的使用。在这个测试中,你将不会测试输出,而是测试是否检测到错误。

检查对分隔符后的选项的检测

你可以先找出当参数包括未定义的选项时会发生什么。可能的选项是在

在你声明一个程序变量之前,你需要让它可用。

const { Command } = require("commander");

Commander.js 建议创建一个本地命令对象,在测试时使用。你将在稍后进行,在每个测试块内进行,像这样。

const program = new Command();

考虑一下下面的测试片段。

/** handle-errors.spec.js **/
test("when arguments includes -- then stop processing options", () => {
  const program = new Command();
  program
    .option("-c, --coatings [value]", "cake coatings to apply")
    .option("-t, --type <cake-type>", "specify the type of cake");
  program.parse([
    "node",
    "palatial-cakes-cli",
    "--coatings",
    "--",
    "--type",
    "order-cake",
  ]);
  const opts = program.opts();
  expect(opts.coatings).toBe(true);
  expect(opts.type).toBeUndefined();
  expect(program.args).toEqual(["--type", "order-cake"]);
});

第一个 非选项-参数的参数应该被接受为表示选项结束的分隔符。即使它们以- 字符开始,任何后续参数都应该被视为操作数。

因为—-coatings 选项在-- 之前,所以它被视为一个有效的选项,你可以执行一个断言来看看这是否正确。在这之后的-–type 应该被当作一个undefined 选项。

Valid and invalid CLI options

program.parse(arguments) 将对参数进行处理,任何没有被程序接收的选项将被留在 数组中。因为 选项被忽略了,你可以断定它属于 数组中。program.args –type program.args

检查对未知选项的检测

/** handle-errors.spec.js **/
test("unknown option, then handle error", () => {
  const program = new Command();
  program
    .exitOverride()
    .command("order-cake")
    .action(() => {});
  let caughtErr;
  try {
    program.parse(["node", "palatial-cakes-cli", "order-cake", "--color"]);
  } catch (err) {
    caughtErr = err;
  }
  expect(caughtErr.code).toBe("commander.unknownOption");
});

在上面的测试块中没有什么不正常的地方。它只是向program.parse() 方法传递了一个未知的选项–-color ,并检查是否被检测到错误。你可以确认这个错误是一个commander.unknownOption 的实例,正如预期的那样。

然后通过整齐地把它记录到终端来处理这个错误。如果你在handle-errors.spec.js 文件内用上述代码块运行npm test ,你应该得到一个通过的测试,错误信息如下。error: unknown option '--color'.

检查对未知命令的检测

就像你测试一个未知的选项一样,你可以检测一个未知的命令,然后整齐地在终端记录一个错误信息。

下面是测试片段的实现。

/** handle-errors.spec.js **/
test("unknown command, then handle error", () => {
  const program = new Command();
  program
    .exitOverride()
    .command("order-cake")
    .action(() => {});
  let caughtErr;
  try {
    program.parse(["node", "palatial-cakes-cli", "make-order"]);
  } catch (err) {
    caughtErr = err;
  }
  expect(caughtErr.code).toBe("commander.unknownCommand");
});

这提供了program.parse() 方法中预期的参数列表,但有一个未知的CLI命令make-order 。你可以期望抛出的错误是commander.unknownCommand 的一个实例,所以你可以针对这个错误断言。在CI环境中设置测试之前,运行你的测试应该验证它们在本地是否全部通过。

在下一节,你将把你的GitHub账户连接到CircleCI。然后,你将把你写的所有测试推送到你的Github账户,以便你可以配置它们在CI环境中运行。

配置CircleCI

为了配置CircleCI,创建一个名为.circleci 的目录并添加一个名为config.yml 的文件。在.circleci/config.yml 文件中,添加这个配置。

# .circleci/config.yml
version: 2.1
jobs:
  build:
    working_directory: ~/palatial-cakes-cli-app
    docker:
      - image: cimg/node:10.16.3
    steps:
      - checkout
      - run:
          name: update npm
          command: "npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: run tests
          command: npm test
      - store_artifacts:
          path: ~/palatial-cakes-cli-app/__tests__

现在你可以提交并推送你的修改到版本库。然后在CircleCI仪表板上设置你的项目。

在 CircleCI 仪表板上,进入项目。所有与你的GitHub用户名或组织相关的GitHub仓库都会被列出。本教程的仓库是palatial-cakes-cli-app 。在 "项目 "仪表板上,选择选项来设置项目。使用分支中的现有配置的选项main

Setting up CircleCI

Voila!在检查CircleCI仪表板并展开构建细节时,你可以验证你已经成功地运行你的CLI应用测试,并将它们集成到CircleCI。

Successful test execution

现在,任何时候你对你的应用程序进行更改,CircleCI将自动运行你的测试,并验证所做的更改是否会破坏你的应用程序。

总结

在本教程中,你已经了解了CLI应用程序和它们的工作原理。你设置了一个简单的CLI应用程序,了解它是如何工作的,并为它写了测试。然后,您配置了CircleCI来运行您的测试,并验证了这些测试的成功。

虽然样本应用程序已经创建和配置好了,但可能值得去看一下这些文件。这可能会帮助你更好地理解当CLI应用程序被commander.js ,在引擎下发生了什么。

一如既往,我很喜欢为你创建这个教程,我希望你能发现它的价值。在下一次学习之前,请继续学习,继续构建!


Waweru Mwaura是一名软件工程师和终身学习者,专门从事质量工程。他是Packt的一名作者,喜欢阅读关于工程、金融和技术的文章。你可以在他的网站简介中了解更多关于他的信息。