如何使用Mocha和Assert测试Node.js模块
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
测试是软件开发的一个组成部分。 程序员在进行更改时运行代码来测试他们的应用程序以确认其行为是否符合他们的意愿是很常见的。 通过正确的测试设置,这个过程甚至可以自动化,从而节省大量时间。 在编写新代码后始终如一地运行测试可确保新更改不会破坏预先存在的功能。 这让开发人员对他们的代码库充满信心,特别是当它被部署到生产环境中以便用户可以与之交互时。
测试框架 构建了我们创建测试用例的方式。 Mocha 是一个流行的 JavaScript 测试框架,它组织我们的测试用例并为我们运行它们。 但是,Mocha 不会验证我们代码的行为。 要在测试中比较值,我们可以使用 Node.js assert 模块。
在本文中,您将为 Node.js TODO 列表模块编写测试。 您将设置并使用 Mocha 测试框架来构建您的测试。 然后,您将使用 Node.js assert
模块自行创建测试。 从这个意义上说,您将使用 Mocha 作为计划构建器,并使用 assert
来实施计划。
先决条件
- Node.js 安装在您的开发机器上。 本教程使用 Node.js 版本 10.16.0。 要在 macOS 或 Ubuntu 18.04 上安装它,请按照 如何在 macOS 上安装 Node.js 和创建本地开发环境中的步骤或 的 使用 PPA 部分安装如何在 Ubuntu 18.04 上安装 Node.js。
- JavaScript 的基本知识,您可以在我们的 如何在 JavaScript 中编写代码系列 中找到。
第 1 步 — 编写节点模块
让我们从编写我们想要测试的 Node.js 模块开始这篇文章。 该模块将管理 TODO 项目列表。 使用这个模块,我们将能够列出我们正在跟踪的所有 TODO,添加新项目,并将一些标记为完成。 此外,我们将能够将 TODO 项目列表导出到 CSV 文件。 如果您想复习一下如何编写 Node.js 模块,可以阅读我们关于 如何创建 Node.js 模块 的文章。
首先,我们需要设置编码环境。 在终端中使用您的项目名称创建一个文件夹。 本教程将使用名称 todos
:
mkdir todos
然后进入那个文件夹:
cd todos
现在初始化 npm,因为我们稍后将使用它的 CLI 功能来运行测试:
npm init -y
我们只有一个依赖项 Mocha,我们将使用它来组织和运行我们的测试。 要下载并安装 Mocha,请使用以下命令:
npm i request --save-dev mocha
我们将 Mocha 安装为 dev
依赖项,因为在生产环境中的模块不需要它。 如果您想了解有关 Node.js 包或 npm 的更多信息,请查看我们关于 如何使用带有 npm 和 package.json 的 Node.js 模块的指南。
最后,让我们创建包含模块代码的文件:
touch index.js
有了这个,我们就可以创建我们的模块了。 在像 nano
这样的文本编辑器中打开 index.js
:
nano index.js
让我们从定义 Todos
类 开始。 此类包含我们管理 TODO 列表所需的所有 函数。 将以下代码行添加到 index.js
:
todos/index.js
class Todos { constructor() { this.todos = []; } } module.exports = Todos;
我们通过创建一个 Todos
类来开始文件。 它的 constructor()
函数不带参数,因此我们不需要提供任何值来实例化此类的对象。 当我们初始化一个 Todos
对象时,我们所做的就是创建一个 todos
属性,它是一个空的 array。
modules
行允许其他 Node.js 模块需要我们的 Todos
类。 如果不显式导出类,我们稍后将创建的测试文件将无法使用它。
让我们添加一个函数来返回我们存储的 todos
数组。 写下以下突出显示的行:
todos/index.js
class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } } module.exports = Todos;
我们的 list()
函数返回该类使用的数组的副本。 它使用 JavaScript 的 解构语法 来制作数组的副本。 我们制作了数组的副本,以便用户对 list()
返回的数组所做的更改不会影响 Todos
对象使用的数组。
注意: JavaScript 数组是 引用类型。 这意味着对于任何 variable 赋值给数组或以数组作为参数的函数调用,JavaScript 引用创建的原始数组。 例如,如果我们有一个名为 x
的包含三个项目的数组,并创建一个新变量 y
使得 y = x
、y
和 [ X143X]两者指的是同一件事。 我们使用 y
对数组所做的任何更改都会影响变量 x
,反之亦然。
现在让我们编写 add()
函数,它添加了一个新的 TODO 项:
todos/index.js
class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } add(title) { let todo = { title: title, completed: false, } this.todos.push(todo); } } module.exports = Todos;
我们的 add()
函数接受一个字符串,并将其放在一个新的 JavaScript object 的 title
属性中。 新对象还有一个 completed
属性,默认设置为 false
。 然后我们将这个新对象添加到我们的 TODO 数组中。
TODO 管理器中的重要功能是将项目标记为已完成。 对于这个实现,我们将遍历我们的 todos
数组来查找用户正在搜索的 TODO 项。 如果找到,我们会将其标记为已完成。 如果没有找到,我们将抛出一个错误。
像这样添加 complete()
函数:
todos/index.js
class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } add(title) { let todo = { title: title, completed: false, } this.todos.push(todo); } complete(title) { let todoFound = false; this.todos.forEach((todo) => { if (todo.title === title) { todo.completed = true; todoFound = true; return; } }); if (!todoFound) { throw new Error(`No TODO was found with the title: "${title}"`); } } } module.exports = Todos;
保存文件并退出文本编辑器。
我们现在有了一个可以试验的基本 TODO 管理器。 接下来,让我们手动测试我们的代码,看看应用程序是否正常工作。
第 2 步 — 手动测试代码
在这一步中,我们将运行代码的函数并观察输出以确保它符合我们的预期。 这称为 手动测试 。 这可能是程序员最常用的测试方法。 尽管稍后我们将使用 Mocha 自动化我们的测试,但我们将首先手动测试我们的代码,以便更好地了解手动测试与测试框架的不同之处。
让我们将两个 TODO 项添加到我们的应用程序中,并将其中一项标记为完成。 在与 index.js
文件相同的文件夹中启动 Node.js REPL:
node
您将在 REPL 中看到 >
提示,告诉我们可以输入 JavaScript 代码。 在提示符处键入以下内容:
const Todos = require('./index');
使用 require()
,我们将 TODOs 模块加载到 Todos
变量中。 回想一下,我们的模块默认返回 Todos
类。
现在,让我们为该类实例化一个对象。 在 REPL 中,添加这行代码:
const todos = new Todos();
我们可以使用 todos
对象来验证我们的实现是否有效。 让我们添加我们的第一个 TODO 项:
todos.add("run code");
到目前为止,我们还没有在终端中看到任何输出。 让我们通过获取所有 TODO 的列表来验证我们是否存储了 "run code"
TODO 项:
todos.list();
你会在你的 REPL 中看到这个输出:
Output[ { title: 'run code', completed: false } ]
这是预期的结果:我们的 TODO 数组中有一个 TODO 项,默认情况下它没有完成。
让我们添加另一个 TODO 项:
todos.add("test everything");
将第一个 TODO 项目标记为已完成:
todos.complete("run code");
我们的 todos
对象现在将管理两个项目:"run code"
和 "test everything"
。 "run code"
TODO 也将完成。 让我们再次调用 list()
来确认这一点:
todos.list();
REPL 将输出:
Output[ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]
现在,使用以下命令退出 REPL:
.exit
我们已经确认我们的模块的行为符合我们的预期。 虽然我们没有将代码放入测试文件或使用测试库,但我们确实手动测试了我们的代码。 不幸的是,每次我们进行更改时,这种形式的测试都会变得很耗时。 接下来,让我们在 Node.js 中使用自动化测试,看看我们是否可以使用 Mocha 测试框架解决这个问题。
第三步——用 Mocha 和 Assert 编写你的第一个测试
在最后一步中,我们手动测试了我们的应用程序。 这将适用于个别用例,但随着我们的模块扩展,这种方法变得不那么可行。 当我们测试新功能时,我们必须确定添加的功能没有在旧功能中产生问题。 我们希望针对代码中的每次更改重新测试每个功能,但是手动执行此操作会花费大量精力并且容易出错。
更有效的做法是设置 自动测试 。 这些是像任何其他代码块一样编写的脚本测试。 我们使用定义的输入运行我们的函数并检查它们的效果以确保它们的行为符合我们的预期。 随着我们的代码库增长,我们的自动化测试也会增长。 当我们在功能旁边编写新测试时,我们可以验证整个模块是否仍然有效——所有这些都无需记住每次如何使用每个功能。
在本教程中,我们将 Mocha 测试框架与 Node.js assert
模块一起使用。 让我们获得一些实践经验,看看它们是如何协同工作的。
首先,创建一个新文件来存储我们的测试代码:
touch index.test.js
现在使用您喜欢的文本编辑器打开测试文件。 您可以像以前一样使用 nano
:
nano index.test.js
在文本文件的第一行,我们将像在 Node.js shell 中一样加载 TODOs 模块。 然后,我们将在编写测试时加载 assert
模块。 添加以下行:
todos/index.test.js
const Todos = require('./index'); const assert = require('assert').strict;
assert
模块的 strict
属性将允许我们使用 Node.js 推荐的特殊相等测试,并且有利于未来的验证,因为它们考虑了更多用例。
在我们开始编写测试之前,让我们讨论一下 Mocha 如何组织我们的代码。 在 Mocha 中构建的测试通常遵循以下模板:
describe([String with Test Group Name], function() { it([String with Test Name], function() { [Test Code] }); });
注意两个关键功能:describe()
和 it()
。 describe()
函数用于对相似的测试进行分组。 Mocha 不需要运行测试,但是分组测试使我们的测试代码更易于维护。 建议您以一种便于您将相似的测试一起更新的方式对测试进行分组。
it()
包含我们的测试代码。 这是我们与模块函数交互并使用 assert
库的地方。 许多 it()
函数可以在 describe()
函数中定义。
我们在本节中的目标是使用 Mocha 和 assert
来自动化我们的手动测试。 我们将从我们的描述块开始逐步执行此操作。 在模块行之后将以下内容添加到您的文件中:
todos/index.test.js
... describe("integration test", function() { });
使用这个代码块,我们为我们的集成测试创建了一个分组。 单元测试 一次测试一个函数。 集成测试验证模块内或模块之间的功能如何协同工作。 当 Mocha 运行我们的测试时,该描述块中的所有测试都将在 "integration test"
组下运行。
让我们添加一个 it()
函数,这样我们就可以开始测试我们模块的代码了:
todos/index.test.js
... describe("integration test", function() { it("should be able to add and complete TODOs", function() { }); });
请注意我们为测试命名的描述性。 如果有人运行我们的测试,将立即清楚什么是通过或失败。 一个经过良好测试的应用程序通常是一个文档齐全的应用程序,并且测试有时可以成为一种有效的文档。
对于我们的第一个测试,我们将创建一个新的 Todos
对象并验证其中没有项目:
todos/index.test.js
... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); assert.notStrictEqual(todos.list().length, 1); }); });
第一行新代码实例化了一个新的 Todos
对象,就像我们在 Node.js REPL 或其他模块中所做的那样。 在第二个新行中,我们使用 assert
模块。
在 assert
模块中,我们使用 notStrictEqual()
方法。 这个函数有两个参数:我们想要测试的值(称为 actual
值)和我们期望获得的值(称为 expected
值)。 如果两个参数相同,则 notStrictEqual()
会引发错误以使测试失败。
保存并退出index.test.js
。
基本情况为真,因为长度应为 0
,而不是 1
。 让我们通过运行 Mocha 来确认这一点。 为此,我们需要修改我们的 package.json
文件。 使用文本编辑器打开 package.json
文件:
nano package.json
现在,在您的 scripts
属性中,将其更改为如下所示:
todos/package.json
... "scripts": { "test": "mocha index.test.js" }, ...
我们刚刚更改了 npm 的 CLI test
命令的行为。 当我们运行 npm test
时,npm 会检查我们刚刚在 package.json
中输入的命令。 它将在我们的 node_modules
文件夹中查找 Mocha 库,并使用我们的测试文件运行 mocha
命令。
保存并退出package.json
。
让我们看看运行测试时会发生什么。 在您的终端中,输入:
npm test
该命令将产生以下输出:
Output> todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)
这个输出首先向我们展示了它将运行哪组测试。 对于组中的每个单独测试,测试用例都会缩进。 我们看到了我们在 it()
函数中描述的测试名称。 测试用例左侧的勾号表示测试通过。
在底部,我们得到了所有测试的摘要。 在我们的例子中,我们的一项测试通过并在 16 毫秒内完成(时间因计算机而异)。
我们的测试已经开始成功。 但是,当前的测试用例可能会出现误报。 假阳性是一个测试用例,它应该失败时通过。
我们目前检查数组的长度是否不等于 1
。 让我们修改测试,使这个条件在不应该成立时成立。 将以下行添加到 index.test.js
:
todos/index.test.js
... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); todos.add("get up from bed"); todos.add("make up bed"); assert.notStrictEqual(todos.list().length, 1); }); });
保存并退出文件。
我们添加了两个 TODO 项目。 让我们运行测试看看会发生什么:
npm test
这将给出以下内容:
Output... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)
这按预期通过,因为长度大于 1。 但是,它违背了进行第一次测试的最初目的。 第一个测试旨在确认我们从空白状态开始。 更好的测试将在所有情况下证实这一点。
让我们更改测试,使其只有在我们绝对没有 TODO 存储时才能通过。 对 index.test.js
进行以下更改:
todos/index.test.js
... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); todos.add("get up from bed"); todos.add("make up bed"); assert.strictEqual(todos.list().length, 0); }); });
您将 notStrictEqual()
更改为 strictEqual()
,这是一个检查其实际参数和预期参数是否相等的函数。 如果我们的论点不完全相同,严格相等将失败。
保存并退出,然后运行测试,这样我们就可以看到会发生什么:
npm test
这一次,输出将显示错误:
Output... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.
该文本将有助于我们调试测试失败的原因。 请注意,由于测试失败,因此测试用例开始时没有刻度。
我们的测试摘要不再位于输出的底部,而是显示在我们的测试用例列表之后:
... 0 passing (29ms) 1 failing ...
剩余的输出为我们提供了有关我们失败的测试的数据。 首先,我们看看哪个测试用例失败了:
... 1) integrated test should be able to add and complete TODOs: ...
然后,我们看到了为什么我们的测试失败了:
... AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) ...
当 strictEqual()
失败时抛出 AssertionError
。 我们看到 expected
值 0 与 actual
值 2 不同。
然后我们在测试文件中看到代码失败的行。 在这种情况下,它是第 10 行。
现在,我们已经亲眼看到,如果我们预期不正确的值,我们的测试将会失败。 让我们将测试用例改回正确的值。 首先,打开文件:
nano index.test.js
然后取出 todos.add
行,使您的代码如下所示:
todos/index.test.js
... describe("integration test", function () { it("should be able to add and complete TODOs", function () { let todos = new Todos(); assert.strictEqual(todos.list().length, 0); }); });
保存并退出文件。
再次运行它以确认它通过而没有任何潜在的误报:
npm test
输出如下:
Output... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)
我们现在已经大大提高了测试的弹性。 让我们继续我们的集成测试。 下一步是向 index.test.js
添加一个新的 TODO 项:
todos/index.test.js
... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); assert.strictEqual(todos.list().length, 0); todos.add("run code"); assert.strictEqual(todos.list().length, 1); assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]); }); });
使用 add()
函数后,我们确认我们现在有一个 TODO 由我们的 todos
对象和 strictEqual()
管理。 我们的下一个测试用 deepStrictEqual()
确认 todos
中的数据。 deepStrictEqual()
函数递归地测试我们的预期对象和实际对象是否具有相同的属性。 在这种情况下,它会测试我们期望的数组中是否都有一个 JavaScript 对象。 然后检查它们的 JavaScript 对象是否具有相同的属性,即它们的 title
属性都是 "run code"
并且它们的 completed
属性都是 false
。
然后,我们通过添加以下突出显示的行,根据需要使用这两个相等检查来完成剩余的测试:
todos/index.test.js
... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); assert.strictEqual(todos.list().length, 0); todos.add("run code"); assert.strictEqual(todos.list().length, 1); assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]); todos.add("test everything"); assert.strictEqual(todos.list().length, 2); assert.deepStrictEqual(todos.list(), [ { title: "run code", completed: false }, { title: "test everything", completed: false } ] ); todos.complete("run code"); assert.deepStrictEqual(todos.list(), [ { title: "run code", completed: true }, { title: "test everything", completed: false } ] ); }); });
保存并退出文件。
我们的测试现在模仿我们的手动测试。 使用这些程序化测试,如果我们的测试在运行时通过,我们就不需要连续检查输出。 您通常希望测试使用的各个方面,以确保正确测试代码。
让我们再次使用 npm test
运行我们的测试以获得这个熟悉的输出:
Output... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)
您现在已经使用 Mocha 框架和 assert
库设置了一个集成测试。
让我们考虑一下我们与其他一些开发人员共享我们的模块并且他们现在正在给我们反馈的情况。 如果到目前为止还没有添加 TODO,我们的很大一部分用户希望 complete()
函数返回错误。 让我们在我们的 complete()
函数中添加这个功能。
在文本编辑器中打开 index.js
:
nano index.js
将以下内容添加到函数中:
todos/index.js
... complete(title) { if (this.todos.length === 0) { throw new Error("You have no TODOs stored. Why don't you add one first?"); } let todoFound = false this.todos.forEach((todo) => { if (todo.title === title) { todo.completed = true; todoFound = true; return; } }); if (!todoFound) { throw new Error(`No TODO was found with the title: "${title}"`); } } ...
保存并退出文件。
现在让我们为这个新特性添加一个新的测试。 我们想验证如果我们在没有项目的 Todos
对象上调用完成,它将返回我们的特殊错误。
回到 index.test.js
:
nano index.test.js
在文件末尾,添加以下代码:
todos/index.test.js
... describe("complete()", function() { it("should fail if there are no TODOs", function() { let todos = new Todos(); const expectedError = new Error("You have no TODOs stored. Why don't you add one first?"); assert.throws(() => { todos.complete("doesn't exist"); }, expectedError); }); });
我们像以前一样使用 describe()
和 it()
。 我们的测试从创建一个新的 todos
对象开始。 然后我们定义我们在调用 complete()
函数时期望收到的错误。
接下来,我们使用assert
模块的throws()
函数。 创建了这个函数,以便我们可以验证代码中抛出的错误。 它的第一个参数是一个包含引发错误的代码的函数。 第二个参数是我们期望收到的错误。
在您的终端中,再次使用 npm test
运行测试,您现在将看到以下输出:
Output... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)
此输出突出了我们使用 Mocha 和 assert
进行自动化测试的好处。 因为我们的测试是脚本化的,所以每次我们运行 npm test
时,我们都会验证我们所有的测试都通过了。 我们不需要手动检查其他代码是否仍在工作; 我们知道那是因为我们仍然通过了测试。
至此,我们的测试已经验证了同步代码的结果。 让我们看看我们需要如何调整我们新发现的测试习惯以使用异步代码。
第 4 步 — 测试异步代码
我们在 TODO 模块中想要的功能之一是 CSV 导出功能。 这会将我们存储的所有 TODO 连同完成的状态打印到一个文件中。 这要求我们使用 fs
模块——一个用于处理文件系统的内置 Node.js 模块。
写入文件是异步操作。 在 Node.js 中有很多方法可以写入文件。 我们可以使用回调、Promises 或 async
/await
关键字。 在本节中,我们将看看我们如何为这些不同的方法编写测试。
回调
callback 函数是用作异步函数的参数的函数。 当异步操作完成时调用它。
让我们向我们的 Todos
类添加一个名为 saveToFile()
的函数。 这个函数将通过遍历我们所有的 TODO 项并将该字符串写入文件来构建一个字符串。
打开您的 index.js
文件:
nano index.js
在此文件中,添加以下突出显示的代码:
todos/index.js
const fs = require('fs'); class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } add(title) { let todo = { title: title, completed: false, } this.todos.push(todo); } complete(title) { if (this.todos.length === 0) { throw new Error("You have no TODOs stored. Why don't you add one first?"); } let todoFound = false this.todos.forEach((todo) => { if (todo.title === title) { todo.completed = true; todoFound = true; return; } }); if (!todoFound) { throw new Error(`No TODO was found with the title: "${title}"`); } } saveToFile(callback) { let fileContents = 'Title,Completed\n'; this.todos.forEach((todo) => { fileContents += `${todo.title},${todo.completed}\n` }); fs.writeFile('todos.csv', fileContents, callback); } } module.exports = Todos;
我们首先必须在我们的文件中导入 fs
模块。 然后我们添加了新的 saveToFile()
函数。 我们的函数采用一个回调函数,一旦文件写入操作完成,就会使用该回调函数。 在该函数中,我们创建了一个 fileContents
变量来存储我们想要保存为文件的整个字符串。 它使用 CSV 的标头初始化。 然后我们使用内部数组的 forEach()
方法遍历每个 TODO 项。 当我们迭代时,我们添加了各个 todos
对象的 title
和 completed
属性。
最后,我们使用fs
模块用writeFile()
函数写入文件。 我们的第一个参数是文件名:todos.csv
。 第二个是文件的内容,在本例中是我们的 fileContents
变量。 我们的最后一个参数是我们的回调函数,它处理任何文件写入错误。
保存并退出文件。
现在让我们为我们的 saveToFile()
函数编写一个测试。 我们的测试将做两件事:首先确认文件存在,然后验证它的内容是否正确。
打开index.test.js
文件:
nano index.test.js
让我们首先在文件顶部加载 fs
模块,因为我们将使用它来帮助测试我们的结果:
todos/index.test.js
const Todos = require('./index'); const assert = require('assert').strict; const fs = require('fs'); ...
现在,在文件末尾添加我们的新测试用例:
todos/index.test.js
... describe("saveToFile()", function() { it("should save a single TODO", function(done) { let todos = new Todos(); todos.add("save a CSV"); todos.saveToFile((err) => { assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); done(err); }); }); });
像以前一样,我们使用 describe()
将我们的测试与其他测试分开分组,因为它涉及新功能。 it()
功能与我们的其他功能略有不同。 通常,我们使用的回调函数没有参数。 这一次,我们将 done
作为参数。 当测试带有回调的函数时,我们需要这个参数。 Mocha 使用 done()
回调函数来告诉它异步函数何时完成。
在 Mocha 中测试的所有回调函数都必须调用 done()
回调。 如果没有,Mocha 永远不会知道函数何时完成,并且会一直等待信号。
继续,我们创建我们的 Todos
实例并向其添加单个项目。 然后我们调用 saveToFile()
函数,并带有一个捕获文件写入错误的回调。 请注意我们对此函数的测试如何驻留在回调中。 如果我们的测试代码在回调之外,只要在文件写入完成之前调用代码,它就会失败。
在我们的回调函数中,我们首先检查我们的文件是否存在:
todos/index.test.js
... assert.strictEqual(fs.existsSync('todos.csv'), true); ...
如果参数中的文件路径存在,则 fs.existsSync()
函数返回 true
,否则返回 false
。
<$>[注] 笔记: 这fs
模块的函数默认是异步的。 然而,对于关键功能,他们做了同步对应。 通过使用同步函数,这个测试更简单,因为我们不必嵌套异步代码来确保它工作。 在 fs
模块中,同步函数的名称通常以 "Sync"
结尾。 <$>
然后我们创建一个变量来存储我们的期望值:
todos/index.test.js
... let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; ...
我们使用fs
模块的readFileSync()
来同步读取文件:
todos/index.test.js
... let content = fs.readFileSync("todos.csv").toString(); ...
我们现在为 readFileSync()
提供文件的正确路径:todos.csv
。 由于 readFileSync()
返回一个存储二进制数据的 Buffer
对象,我们使用它的 toString()
方法,以便我们可以将它的值与我们期望保存的字符串进行比较。
和之前一样,我们使用 assert
模块的 strictEqual
做一个比较:
todos/index.test.js
... assert.strictEqual(content, expectedFileContents); ...
我们通过调用 done()
回调来结束我们的测试,确保 Mocha 知道停止测试这种情况:
todos/index.test.js
... done(err); ...
我们为 done()
提供了 err
对象,这样 Mocha 可以在发生错误的情况下使测试失败。
保存并退出index.test.js
。
让我们像以前一样用 npm test
运行这个测试。 您的控制台将显示以下输出:
Output... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)
您现在已经使用回调测试了第一个使用 Mocha 的异步函数。 但是在编写本教程时,Promise 在新的 Node.js 代码中比回调更普遍,正如我们的 如何在 Node.js 中编写异步代码 文章中所解释的。 接下来,让我们学习如何使用 Mocha 来测试它们。
承诺
Promise 是一个 JavaScript 对象,它最终会返回一个值。 当一个 Promise 成功时,它就被解决了。 当它遇到错误时,它会被拒绝。
让我们修改 saveToFile()
函数,使其使用 Promises 而不是回调。 打开index.js
:
nano index.js
首先,我们需要更改 fs
模块的加载方式。 在您的 index.js
文件中,将文件顶部的 require()
语句更改为如下所示:
todos/index.js
... const fs = require('fs').promises; ...
我们刚刚导入了使用 Promises 而不是回调的 fs
模块。 现在,我们需要对 saveToFile()
进行一些更改,以便它可以与 Promises 一起使用。
在您的文本编辑器中,对 saveToFile()
函数进行以下更改以删除回调:
todos/index.js
... saveToFile() { let fileContents = 'Title,Completed\n'; this.todos.forEach((todo) => { fileContents += `${todo.title},${todo.completed}\n` }); return fs.writeFile('todos.csv', fileContents); } ...
第一个区别是我们的函数不再接受任何参数。 使用 Promises,我们不需要回调函数。 第二个更改涉及文件的写入方式。 我们现在返回 writeFile()
承诺的结果。
保存并关闭 index.js
。
现在让我们调整我们的测试,使其适用于 Promises。 打开index.test.js
:
nano index.test.js
将 saveToFile()
测试更改为:
todos/index.js
... describe("saveToFile()", function() { it("should save a single TODO", function() { let todos = new Todos(); todos.add("save a CSV"); return todos.saveToFile().then(() => { assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); }); });
我们需要做的第一个更改是从其参数中删除 done()
回调。 如果 Mocha 传递了 done()
参数,则需要调用它,否则会抛出如下错误:
1) saveToFile() should save a single TODO: Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js) at listOnTimeout (internal/timers.js:536:17) at processTimers (internal/timers.js:480:7)
测试 Promise 时,不要在 it()
中包含 done()
回调。
为了测试我们的 Promise,我们需要将我们的断言代码放在 then()
函数中。 请注意,我们在测试中返回了这个 Promise,当 Promise
被拒绝时,我们没有 catch()
函数可以捕获。
我们返回承诺,以便在 then()
函数中抛出的任何错误都会冒泡到 it()
函数。 如果错误没有冒泡,Mocha 不会使测试用例失败。 测试 Promise 时,需要在被测试的 Promise
上使用 return
。 如果没有,您将面临误报的风险。
我们还省略了 catch()
子句,因为 Mocha 可以检测何时拒绝承诺。 如果被拒绝,它会自动通过测试。
现在我们已经完成了测试,保存并退出文件,然后使用 npm test
运行 Mocha 并确认我们获得了成功的结果:
Output... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)
我们已经更改了代码和测试以使用 Promises,现在我们确定它可以工作。 但是最新的异步模式使用 async
/await
关键字,因此我们不必创建多个 then()
函数来处理成功的结果。 让我们看看如何使用 async
/await
进行测试。
异步/等待
async
/await
关键字使使用 Promise 变得不那么冗长。 一旦我们使用 async
关键字将函数定义为异步,我们就可以使用 await
关键字在该函数中获得任何未来结果。 这样我们就可以使用 Promises 而不必使用 then()
或 catch()
函数。
我们可以简化基于 async
/await
的 promise 的 saveToFile()
测试。 在您的文本编辑器中,对 index.test.js
中的 saveToFile()
测试进行以下小修改:
todos/index.test.js
... describe("saveToFile()", function() { it("should save a single TODO", async function() { let todos = new Todos(); todos.add("save a CSV"); await todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); });
第一个变化是 it()
函数使用的函数现在在定义时具有 async
关键字。 这允许我们在其主体中使用 await
关键字。
当我们调用 saveToFile()
时,会发现第二个变化。 await
关键字在调用之前使用。 现在 Node.js 知道要等到该函数解决后再继续测试。
现在我们的函数代码更易于阅读,因为我们将 then()
函数中的代码移到了 it()
函数的主体中。 使用 npm test
运行此代码会产生以下输出:
Output... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)
我们现在可以适当地使用三种异步范式中的任何一种来测试异步函数。
我们已经用 Mocha 测试同步和异步代码覆盖了很多领域。 接下来,让我们更深入地了解 Mocha 提供的其他一些功能,以改善我们的测试体验,特别是钩子如何改变测试环境。
第 5 步 — 使用 Hooks 改进测试用例
Hooks 是 Mocha 的一个有用功能,它允许我们在测试之前和之后配置环境。 我们通常在 describe()
功能块中添加钩子,因为它们包含特定于某些测试用例的设置和拆卸逻辑。
Mocha 提供了四个可以在测试中使用的钩子:
before
:这个钩子在第一次测试开始前运行一次。beforeEach
:这个钩子在每个测试用例之前运行。after
:这个钩子在最后一个测试用例完成后运行一次。afterEach
:这个钩子在每个测试用例之后运行。
当我们多次测试一个函数或特性时,钩子会派上用场,因为它们允许我们将测试的设置代码(如创建 todos
对象)与测试的断言代码分开。
要查看钩子的价值,让我们在 saveToFile()
测试块中添加更多测试。
虽然我们已经确认可以将 TODO 项保存到文件中,但我们只保存了一项。 此外,该项目未标记为已完成。 让我们添加更多测试以确保我们模块的各个方面都能正常工作。
首先,让我们添加第二个测试,以确认当我们完成一个 TODO 项目时我们的文件是否正确保存。 在文本编辑器中打开 index.test.js
文件:
nano index.test.js
将最后一个测试更改为以下内容:
todos/index.test.js
... describe("saveToFile()", function () { it("should save a single TODO", async function () { let todos = new Todos(); todos.add("save a CSV"); await todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); it("should save a single TODO that's completed", async function () { let todos = new Todos(); todos.add("save a CSV"); todos.complete("save a CSV"); await todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,true\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); });
测试与我们之前的测试类似。 主要区别在于我们在调用 saveToFile()
之前调用了 complete()
函数,并且我们的 expectedFileContents
现在具有 true
而不是 false
对于 completed
列的值。
保存并退出文件。
让我们用 npm test
运行我们的新测试和所有其他测试:
npm test
这将给出以下内容:
Output... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)
它按预期工作。 然而,还有改进的余地。 他们都必须在测试开始时实例化一个 Todos
对象。 随着我们添加更多的测试用例,这很快就会变得重复和浪费内存。 此外,每次我们运行测试时,它都会创建一个文件。 这可能会被不太熟悉该模块的人误认为是实际输出。 如果我们在测试后清理我们的输出文件会很好。
让我们使用测试挂钩进行这些改进。 我们将使用 beforeEach()
挂钩来设置 TODO 项的 测试夹具。 测试夹具是测试中使用的任何一致状态。 在我们的例子中,我们的测试夹具是一个新的 todos
对象,其中已经添加了一个 TODO 项。 然后我们将使用 afterEach()
删除测试创建的文件。
在 index.test.js
中,对 saveToFile()
的上次测试进行以下更改:
todos/index.test.js
... describe("saveToFile()", function () { beforeEach(function () { this.todos = new Todos(); this.todos.add("save a CSV"); }); afterEach(function () { if (fs.existsSync("todos.csv")) { fs.unlinkSync("todos.csv"); } }); it("should save a single TODO without error", async function () { await this.todos.saveToFile(); assert.strictEqual(fs.existsSync("todos.csv"), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); it("should save a single TODO that's completed", async function () { this.todos.complete("save a CSV"); await this.todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,true\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); });
让我们分解一下我们所做的所有更改。 我们在测试块中添加了一个 beforeEach()
块:
todos/index.test.js
... beforeEach(function () { this.todos = new Todos(); this.todos.add("save a CSV"); }); ...
这两行代码创建了一个新的 Todos
对象,该对象将在我们的每个测试中可用。 对于 Mocha,beforeEach()
中的 this
对象指的是 it()
中相同的 this
对象。 this
对于 describe()
块内的每个代码块都是相同的。 有关 this
的更多信息,请参阅我们的教程 Understanding This, Bind, Call, and Apply in JavaScript。
这种强大的上下文共享是我们可以快速创建适用于我们两个测试的测试夹具的原因。
然后我们在 afterEach()
函数中清理我们的 CSV 文件:
todos/index.test.js
... afterEach(function () { if (fs.existsSync("todos.csv")) { fs.unlinkSync("todos.csv"); } }); ...
如果我们的测试失败,那么它可能没有创建文件。 这就是为什么我们在使用 unlinkSync()
函数删除它之前检查文件是否存在的原因。
其余更改将引用从之前在 it()
函数中创建的 todos
切换到在 Mocha 上下文中可用的 this.todos
。 我们还删除了之前在各个测试用例中实例化 todos
的行。
现在,让我们运行这个文件来确认我们的测试仍然有效。 在终端输入 npm test
得到:
Output... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)
结果是相同的,并且作为一个好处,我们略微减少了 saveToFile()
功能的新测试的设置时间,并找到了残留 CSV 文件的解决方案。
结论
在本教程中,您编写了一个 Node.js 模块来管理 TODO 项并使用 Node.js REPL 手动测试代码。 然后,您创建了一个测试文件并使用 Mocha 框架运行自动化测试。 使用 assert
模块,您可以验证您的代码是否有效。 您还使用 Mocha 测试了同步和异步函数。 最后,您使用 Mocha 创建了钩子,使编写多个相关的测试用例更具可读性和可维护性。
有了这种理解,挑战自己,为您正在创建的新 Node.js 模块编写测试。 在编写代码之前,您可以考虑函数的输入和输出并编写测试吗?
如果您想了解更多有关 Mocha 测试框架的信息,请查看 官方 Mocha 文档 。 如果您想继续学习 Node.js,可以返回 如何在 Node.js 中编写代码系列页面。