如何使用 Mocha 和 Assert 测试 Node.js 模块

作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。

介绍

测试是软件开发的一个组成部分,程序员通常会运行代码来测试他们的应用程序,因为他们会做出更改,以确认它是以他们想要的方式进行的。有了正确的测试设置,这个过程甚至可以自动化,节省大量的时间。在编写新代码后持续运行测试,确保新更改不会破坏现有的功能。

一个 test framework 结构我们创建测试案例的方式。 Mocha是一个流行的JavaScript测试框架,它组织我们的测试案例并为我们运行它们。

在本文中,您将为 Node.js TODO 列表模块编写测试。您将设置并使用 Mocha 测试框架来构建您的测试。

前提条件

步骤 1 - 写一个节点模块

让我们从写下我们想要测试的 Node.js 模块开始这篇文章. 该模块将管理一份 TODO 项目列表. 使用此模块,我们将能够列出我们正在跟踪的所有 TODO,添加新项目,并将一些项目标记为完整。 此外,我们将能够将 TODO 项目列表导出到 CSV 文件中。

首先,我们需要设置编码环境。在您的终端中创建一个名为您的项目的文件夹。本教程将使用名称 todos:

1mkdir todos

然后输入这个文件夹:

1cd todos

现在初始化 npm,因为我们将使用其 CLI 功能来运行测试:

1npm init -y

我们只有一种依赖性,Mocha,我们将使用它来组织和运行我们的测试。

1npm i request --save-dev mocha

如果您想了解更多关于 Node.js 包或 npm 的信息,请参阅我们的指南 如何使用 Node.js 模块与 npm 和 package.json

最后,让我们创建我们的文件,其中将包含我们的模块的代码:

1touch index.js

有了它,我们已经准备好创建我们的模块,在像nano这样的文本编辑器中打开index.js:

1nano index.js

让我们先定义 Todos class。 这个类包含我们需要管理我们的 TODO 列表的所有 函数

1[label todos/index.js]
2class Todos {
3    constructor() {
4        this.todos = [];
5    }
6}
7
8module.exports = Todos;

我们通过创建一个Todos类来开始文件。它的constructor()函数不需要参数,因此我们不需要提供任何值来实例化这个类的对象。

模块行允许其他 Node.js 模块要求我们的 Todos 类,如果不明确导出该类,我们将在以后创建的测试文件将无法使用它。

让我们添加一个函数来返回我们已存储的todos的数组。

 1[label todos/index.js]
 2class Todos {
 3    constructor() {
 4        this.todos = [];
 5    }
 6
 7    list() {
 8        return [...this.todos];
 9    }
10}
11
12module.exports = Todos;

我们的 list() 函数返回了该类所使用的数组的副本. 它使用 JavaScript 的 destructuring syntax来创建数组的副本。

<$>[注] 注: JavaScript 数组是 reference types. 这意味着,对于任何与数组作为参数的数组或函数召唤的 变量的分配,JavaScript 指的是创建的原始数组。

现在,让我们写add()函数,添加一个新的TOTO项目:

 1[label todos/index.js]
 2class Todos {
 3    constructor() {
 4        this.todos = [];
 5    }
 6
 7    list() {
 8        return [...this.todos];
 9    }
10
11    add(title) {
12        let todo = {
13            title: title,
14            completed: false,
15        }
16
17        this.todos.push(todo);
18    }
19}
20
21module.exports = Todos;

我们的add()函数采取一个字符串,并将其放置在一个新的JavaScript object标题属性中。

在 TODO 管理器中,一个重要的功能是将项目标记为完成。对于此实现,我们将通过我们的todos数组来找到用户正在寻找的 TODO 项目。

添加完整()函数如下:

 1[label todos/index.js]
 2class Todos {
 3    constructor() {
 4        this.todos = [];
 5    }
 6
 7    list() {
 8        return [...this.todos];
 9    }
10
11    add(title) {
12        let todo = {
13            title: title,
14            completed: false,
15        }
16
17        this.todos.push(todo);
18    }
19
20    complete(title) {
21        let todoFound = false;
22        this.todos.forEach((todo) => {
23            if (todo.title === title) {
24                todo.completed = true;
25                todoFound = true;
26                return;
27            }
28        });
29
30        if (!todoFound) {
31            throw new Error(`No TODO was found with the title: "${title}"`);
32        }
33    }
34}
35
36module.exports = Todos;

保存文件并离开文本编辑器。

接下来,让我们手动测试我们的代码,看看应用程序是否工作。

步骤 2 – 手动测试代码

在此步骤中,我们将运行我们的代码的功能,并观察输出,以确保它符合我们的期望. 这被称为 manual testing. 它可能是最常见的测试方法程序员应用。

让我们将两个 TODO 项目添加到我们的应用程序中,并将其中一个标记为完整,在同一个文件夹中启动 Node.js REPL:

1node

您将在 REPL 中看到>提示,告诉我们我们可以输入JavaScript代码。

1const Todos = require('./index');

使用require(),我们将 TODOS 模块加载到一个Todos变量中,请记住,我们的模块默认情况下会返回Todos类。

现在,让我们为该类实例化一个对象. 在 REPL 中,添加以下代码行:

1const todos = new Todos();

我们可以使用todos对象来验证我们的实现工作. 让我们添加我们的第一个TODO项目:

1todos.add("run code");

到目前为止,我们在我们的终端中没有看到任何输出。

1todos.list();

您将在您的 REPL 中看到此输出:

1[secondary_label Output]
2[ { title: 'run code', completed: false } ]

这是预期的结果:我们在 TODOS 系列中有一个 TODO 项目,并且默认情况下没有完成。

再加上一个全题:

1todos.add("test everything");

标记第一个全部项目如完成:

1todos.complete("run code");

我们的todos对象现在将管理两个项目:运行代码测试一切

1todos.list();

REPL将产生:

1[secondary_label Output]
2[
3  { title: 'run code', completed: true },
4  { title: 'test everything', completed: false }
5]

现在,离开 REPL 以以下方式:

1.exit

虽然我们没有将我们的代码放入测试文件或使用测试库,但我们确实手动测试了我们的代码。不幸的是,这种形式的测试每次我们进行更改都需要时间。

步骤 3 – 用Mocha和Assert编写你的第一个测试

在最后一步中,我们手动测试了我们的应用程序,这将适用于个别的使用情况,但随着我们的模块规模,这种方法变得不太可行。当我们测试新功能时,我们必须确保增加的功能在旧功能中没有造成问题。

一个更有效的做法是设置自动化测试,这些是写作的脚本测试,就像任何其他代码块一样。我们用定义的输入运行我们的函数,并检查它们的效果,以确保它们的行为如我们所期望的那样。

在本教程中,我们正在使用Mocha测试框架与Node.js肯定模块。

首先,创建一个新的文件来存储我们的测试代码:

1touch index.test.js

现在使用您喜爱的文本编辑器打开测试文件,您可以像以前一样使用nano:

1nano index.test.js

在文本文件的第一行中,我们将像我们在Node.js壳中一样加载TODO模块,然后在我们编写测试时加载声明模块。

1[label todos/index.test.js]
2const Todos = require('./index');
3const assert = require('assert').strict;

声明模块的严格属性将允许我们使用Node.js推荐的特殊平等测试,并且适合未来的验证,因为它们对更多使用案例负责。

在我们进入写作测试之前,让我们讨论Mocha如何组织我们的代码。

1describe([String with Test Group Name], function() {
2    it([String with Test Name], function() {
3        [Test Code]
4    });
5});

请注意两个关键函数: describe()it(). describe() 函数用于组合相似的测试. Mocha 不需要运行测试,但组合测试使我们的测试代码更容易维护。

it()包含我们的测试代码,这就是我们将与模块的函数互动并使用assert库的地方,许多it()函数可以在describe()函数中定义。

在本节中,我们的目标是使用Mocha和‘assert’来自动化我们的手动测试。我们将逐步完成此操作,从我们的描述块开始。

1[label todos/index.test.js]
2...
3describe("integration test", function() {
4});

使用此代码块,我们为我们的集成测试创建了一个组合。 Unit tests 将一次测试一个函数。 Integration tests 验证模块内或模块之间函数的合作程度。

让我们添加一个 it() 函数,这样我们就可以开始测试我们的模块的代码:

1[label todos/index.test.js]
2...
3describe("integration test", function() {
4    it("should be able to add and complete TODOs", function() {
5    });
6});

请注意,我们如何描述测试的名称。如果有人运行我们的测试,它将立即清楚,什么是通过或失败。一个经过测试的应用程序通常是一个经过记录的应用程序,测试有时可以是一个有效的文件类型。

对于我们的第一个测试,我们将创建一个新的Todos对象,并验证它没有内容:

1[label todos/index.test.js]
2...
3describe("integration test", function() {
4    it("should be able to add and complete TODOs", function() {
5        let todos = new Todos();
6        assert.notStrictEqual(todos.list().length, 1);
7    });
8});

第一行新的代码实例化了一个新的Todos对象,就像我们在Node.js REPL或其他模块中所做的那样。

这个函数需要两个参数:我们要测试的值(称为实际值)和我们期望得到的值(称为预期值)。

保存并退出 index.test.js。

基本情况将是真实的,因为长度应该是0,而不是1。让我们通过运行Mocha来确认这一点。 要做到这一点,我们需要修改我们的package.json文件。 使用文本编辑器打开您的package.json文件:

1nano package.json

现在,在你的脚本属性中,改变它看起来像这样:

1[label todos/package.json]
2...
3"scripts": {
4    "test": "mocha index.test.js"
5},
6...

當我們執行「npm test」時,npm 會檢查我們剛在「package.json」中輸入的命令,它會在我們的「node_modules」文件夾中尋找Mocha圖書館,並與我們的測試檔案一起執行「mocha」命令。

保存并退出package.json

让我们看看当我们运行测试时会发生什么。在您的终端中,输入:

1npm test

命令将产生以下输出:

1[secondary_label Output]
2> [email protected] test your_file_path/todos
3> mocha index.test.js
4
5integrated test
6    ✓ should be able to add and complete TODOs
7
8  1 passing (16ms)

这个输出首先向我们展示了它即将运行的测试组。在一个组内每个单独的测试中,测试案例被注明。我们看到我们的测试名称,正如我们在 it() 函数中描述的那样。

在我们的案例中,我们的一个测试正在通过,并在16ms内完成(时间因计算机而异)。

我们的测试已经成功开始,但是,这个当前的测试案例可以允许假阳性。

我们目前正在检查数组的长度是否不等于1。让我们修改测试,以便此条件在不应该时保持正确。

 1[label todos/index.test.js]
 2...
 3describe("integration test", function() {
 4    it("should be able to add and complete TODOs", function() {
 5        let todos = new Todos();
 6        todos.add("get up from bed");
 7        todos.add("make up bed");
 8        assert.notStrictEqual(todos.list().length, 1);
 9    });
10});

保存和退出文件。

我们添加了两个TOTO项目,让我们运行测试,看看会发生什么:

1npm test

这将给出如下:

1[secondary_label Output]
2...
3integrated test
4    ✓ should be able to add and complete TODOs
5
6  1 passing (8ms)

这是按照预期进行的,因为长度大于1。然而,它打败了有第一个测试的原始目的。第一个测试旨在确认我们从空状态开始。

让我们更改测试,以便它只通过,如果我们绝对没有全部存储. 对index.test.js进行以下更改:

 1[label todos/index.test.js]
 2...
 3describe("integration test", function() {
 4    it("should be able to add and complete TODOs", function() {
 5        let todos = new Todos();
 6        todos.add("get up from bed");
 7        todos.add("make up bed");
 8        assert.strictEqual(todos.list().length, 0);
 9    });
10});

您已将notStrictEqual()更改为strictEqual(),该函数检查其实际和预期参数之间的平等,如果我们的参数不完全相同,则严格的等式将失败。

保存和退出,然后运行测试,这样我们就可以看到发生了什么:

1npm test

这次,输出将显示一个错误:

 1[secondary_label Output]
 2...
 3  integration test
 4    1) should be able to add and complete TODOs
 5
 6  0 passing (16ms)
 7  1 failing
 8
 9  1) integration test
10       should be able to add and complete TODOs:
11
12      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
13+ expected - actual
14
15- 2
16+ 0
17      + expected - actual
18
19      -2
20      +0
21
22      at Context.<anonymous> (index.test.js:9:10)
23
24npm ERR! Test failed. See above for more details.

请注意,自从测试失败以来,测试案例开始时没有点滴。

我们的测试摘要不再出现在输出底部,而是在我们的测试案例列表显示之后:

1...
20 passing (29ms)
3  1 failing
4...

剩余的输出为我们提供了关于我们失败的测试的数据,首先,我们看到哪些测试案例失败了:

1...
21) integrated test
3       should be able to add and complete TODOs:
4...

然后,我们看看为什么我们的测试失败了:

 1...
 2      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
 3+ expected - actual
 4
 5- 2
 6+ 0
 7      + expected - actual
 8
 9      -2
10      +0
11
12      at Context.<anonymous> (index.test.js:9:10)
13...

當「strictEqual()」失敗時,會出現「AssertionError」。我們看到「預期」值0與「實際」值2不同。

然后我们在测试文件中看到代码失败的行,在这种情况下,它是行10。

现在,我们已经亲眼看到,如果我们期望错误的值,我们的测试将失败。

1nano index.test.js

然后删除todos.add行,以便您的代码看起来如下:

1[label todos/index.test.js]
2...
3describe("integration test", function () {
4    it("should be able to add and complete TODOs", function () {
5        let todos = new Todos();
6        assert.strictEqual(todos.list().length, 0);
7    });
8});

保存和退出文件。

再次运行它,以确认它没有任何潜在的假阳性:

1npm test

产量将如下:

1[secondary_label Output]
2...
3integration test
4    ✓ should be able to add and complete TODOs
5
6  1 passing (15ms)

现在我们已经改进了测试的弹性,让我们继续进行集成测试,下一步是将一个新的TODO项目添加到index.test.js:

 1[label todos/index.test.js]
 2...
 3describe("integration test", function() {
 4    it("should be able to add and complete TODOs", function() {
 5        let todos = new Todos();
 6        assert.strictEqual(todos.list().length, 0);
 7
 8        todos.add("run code");
 9        assert.strictEqual(todos.list().length, 1);
10        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
11    });
12});

在使用add()函数后,我们确认我们现在有一个由我们的todos对象管理的TODO,使用strictEqual()。我们的下一个测试证实了todos中的数据,使用deepStrictEqual()deepStrictEqual()函数反复测试了我们的预期和实际对象具有相同的属性。在这种情况下,它测试了我们期望的数组中都有JavaScript对象。

然后,我们根据需要使用这两个平等检查完成剩余的测试,添加以下突出线条:

 1[label todos/index.test.js]
 2...
 3describe("integration test", function() {
 4    it("should be able to add and complete TODOs", function() {
 5        let todos = new Todos();
 6        assert.strictEqual(todos.list().length, 0);
 7
 8        todos.add("run code");
 9        assert.strictEqual(todos.list().length, 1);
10        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
11
12        todos.add("test everything");
13        assert.strictEqual(todos.list().length, 2);
14        assert.deepStrictEqual(todos.list(),
15            [
16                { title: "run code", completed: false },
17                { title: "test everything", completed: false }
18            ]
19        );
20
21        todos.complete("run code");
22        assert.deepStrictEqual(todos.list(),
23            [
24                { title: "run code", completed: true },
25                { title: "test everything", completed: false }
26            ]
27    );
28  });
29});

保存和退出文件。

我们的测试现在模仿我们的手动测试. 使用这些程序测试,我们不需要连续检查输出,如果我们的测试在我们运行它们时通过。

让我们用npm 测试再次运行我们的测试,以获得这种熟悉的输出:

1[secondary_label Output]
2...
3integrated test
4    ✓ should be able to add and complete TODOs
5
6  1 passing (9ms)

您现在已经设置了与Mocha框架和肯定库的集成测试。

让我们考虑一个情况,我们已经与一些其他开发者分享了我们的模块,他们现在正在给我们反馈。我们的一部分用户希望完整()函数返回错误,如果没有添加TODO。

在文本编辑器中打开index.js:

1nano index.js

将以下内容添加到函数中:

 1[label todos/index.js]
 2...
 3complete(title) {
 4    if (this.todos.length === 0) {
 5        throw new Error("You have no TODOs stored. Why don't you add one first?");
 6    }
 7
 8    let todoFound = false
 9    this.todos.forEach((todo) => {
10        if (todo.title === title) {
11            todo.completed = true;
12            todoFound = true;
13            return;
14        }
15    });
16
17    if (!todoFound) {
18        throw new Error(`No TODO was found with the title: "${title}"`);
19    }
20}
21...

保存和退出文件。

现在让我们为这个新功能添加一个新的测试,我们想验证,如果我们在没有项目的Todos对象上调用完整,它将返回我们的特殊错误。

回到 index.test.js 中:

1nano index.test.js

在文件的末尾,添加以下代码:

 1[label todos/index.test.js]
 2...
 3describe("complete()", function() {
 4    it("should fail if there are no TODOs", function() {
 5        let todos = new Todos();
 6        const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
 7
 8        assert.throws(() => {
 9            todos.complete("doesn't exist");
10        }, expectedError);
11    });
12});

我们的测试从创建一个新的todos对象开始,然后我们定义了我们在调用complete()函数时所期望的错误。

接下来,我们使用了assert模块的throw()函数,这个函数是创建的,以便我们可以验证我们代码中的错误,它的第一个参数是包含错误的代码的函数,第二个参数是我们期望收到的错误。

在您的终端中,再次运行npm 测试,您将看到以下输出:

1[secondary_label Output]
2...
3integrated test
4    ✓ should be able to add and complete TODOs
5
6  complete()
7    ✓ should fail if there are no TODOs
8
9  2 passing (25ms)

由于我们的测试是脚本化的,每次我们运行npm 测试,我们都验证我们所有的测试都通过了。

到目前为止,我们的测试已经验证了同步代码的结果,让我们看看我们需要如何调整新发现的测试习惯,以便使用不同步代码。

第4步:测试非同步代码

我们在 TODO 模块中想要的功能之一是 CSV 导出功能,这将打印我们存储的所有 TODO 以及完成状态的文件,这需要我们使用fs模块 - 内置的 Node.js 模块来与文件系统工作。

在 Node.js 中写到文件有很多方法,我们可以使用回复,承诺,或async / 等待的关键字。

呼叫

一个 callback 函数是作为一个对非同步函数的参数使用的函数,它在完成非同步操作时被调用。

让我们将一个函数添加到我们的Todos类别中,称为saveToFile()。这个函数将通过循环通过我们的所有TODO项目来构建一个字符串,并将这个字符串写入一个文件中。

打开您的 index.js 文件:

1nano index.js

在此文件中,添加以下突出的代码:

 1[label todos/index.js]
 2const fs = require('fs');
 3
 4class Todos {
 5    constructor() {
 6        this.todos = [];
 7    }
 8
 9    list() {
10        return [...this.todos];
11    }
12
13    add(title) {
14        let todo = {
15            title: title,
16            completed: false,
17        }
18        this.todos.push(todo);
19    }
20
21    complete(title) {
22        if (this.todos.length === 0) {
23            throw new Error("You have no TODOs stored. Why don't you add one first?");
24        }
25
26        let todoFound = false
27        this.todos.forEach((todo) => {
28            if (todo.title === title) {
29                todo.completed = true;
30                todoFound = true;
31                return;
32            }
33        });
34
35        if (!todoFound) {
36            throw new Error(`No TODO was found with the title: "${title}"`);
37        }
38    }
39
40    saveToFile(callback) {
41        let fileContents = 'Title,Completed\n';
42        this.todos.forEach((todo) => {
43            fileContents += `${todo.title},${todo.completed}\n`
44        });
45
46        fs.writeFile('todos.csv', fileContents, callback);
47    }
48}
49
50module.exports = Todos;

我们首先必须在我们的文件中导入fs模块,然后我们添加了我们的新的saveToFile()函数,我们的函数采用回调函数,在完成文件写作操作后将被使用。在该函数中,我们创建了一个fileContents变量,将我们想要保存的整个字符串存储为一个文件。它以 CSV 的标题进行初始化,然后我们通过内部数组的forEach()方法循环每个TODO项目。当我们重复时,我们添加个别todos对象的标题完成属性。

最后,我们使用fs模块来编写文件,使用writeFile()函数。 我们的第一个论点是文件名:todos.csv。 第二个是文件的内容,在这种情况下,我们的fileContents变量。 我们的最后一个论点是我们的回复函数,它处理任何文件编写错误。

保存和退出文件。

现在让我们为我们的saveToFile()函数写一个测试,我们的测试会做两件事:首先确认文件存在,然后确认它有正确的内容。

打开 index.test.js 文件:

1nano index.test.js

让我们开始下载文件顶部的fs模块,因为我们将使用它来测试我们的结果:

1[label todos/index.test.js]
2const Todos = require('./index');
3const assert = require('assert').strict;
4const fs = require('fs');
5...

现在,在文件的末尾,让我们添加我们的新测试案例:

 1[label todos/index.test.js]
 2...
 3describe("saveToFile()", function() {
 4    it("should save a single TODO", function(done) {
 5        let todos = new Todos();
 6        todos.add("save a CSV");
 7        todos.saveToFile((err) => {
 8            assert.strictEqual(fs.existsSync('todos.csv'), true);
 9            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
10            let content = fs.readFileSync("todos.csv").toString();
11            assert.strictEqual(content, expectedFileContents);
12            done(err);
13        });
14    });
15});

就像以前一样,我们使用描述()来将我们的测试与其他测试单独组合,因为它涉及新的功能。it()函数与我们的其他函数略有不同。通常,我们使用的回调函数没有论点。

所有在Mocha中测试的回调函数都必须拨打完成( )回调函数,否则,Mocha永远不会知道函数何时完成,并且会被困在等待信号。

继续,我们创建我们的Todos实例,并添加一个单一的项目,然后我们调用SaveToFile()函数,带回调用来捕捉文件写错误。 请注意,我们对这个函数的测试如何在回调中。

在我们的回复功能中,我们首先检查我们的文件是否存在:

1[label todos/index.test.js]
2...
3assert.strictEqual(fs.existsSync('todos.csv'), true);
4...

fs.existsSync()函数返回true,如果其参数中的文件路径存在,则返回false

<$>[注] 注: fs 模块的功能默认是无同步的,但是对于关键函数,他们制作了同步对象。 这种测试通过使用同步函数更简单,因为我们不需要嵌入无同步代码来确保它工作。 在 fs 模块中,同步函数通常在名称的末尾结束为Sync。 <$>

然后我们创建一个变量来存储我们的预期值:

1[label todos/index.test.js]
2...
3let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
4...

我们使用fs模块的readFileSync()来同步阅读文件:

1[label todos/index.test.js]
2...
3let content = fs.readFileSync("todos.csv").toString();
4...

现在我们为readFileSync()提供文件的正确路径:todos.csv。当readFileSync()返回一个存储二进制数据的缓存对象时,我们使用其toString()方法来比较其值,以便与我们预期已保存的字符串进行比较。

就像以前一样,我们使用肯定模块的strictEqual来进行比较:

1[label todos/index.test.js]
2...
3assert.strictEqual(content, expectedFileContents);
4...

我们通过调用完成( )回调来结束我们的测试,确保Mocha知道如何停止测试该案子:

1[label todos/index.test.js]
2...
3done(err);
4...

我们将err对象指定为done(),以便Mocha在出现错误的情况下可以失败测试。

保存并退出 index.test.js。

让我们像以前那样使用npm test来运行这个测试,您的控制台将显示以下输出:

 1[secondary_label Output]
 2...
 3integrated test
 4    ✓ should be able to add and complete TODOs
 5
 6  complete()
 7    ✓ should fail if there are no TODOs
 8
 9  saveToFile()
10    ✓ should save a single TODO
11
12  3 passing (15ms)

但是在写本教程时,诺言比在新 Node.js 代码中的回调更为普遍,正如我们在 如何在 Node.js 中写非同步代码文章中所解释的那样。

承诺

一个 Promise是一个 JavaScript 对象,最终会返回一个值。当一个 Promise 成功时,它会被解决。

让我们修改saveToFile()函数,以便它使用Promises而不是Callbacks

1nano index.js

首先,我们需要更改fs模块的加载方式,在您的index.js文件中,更改文件顶部的require()语句,看起来像这样:

1[label todos/index.js]
2...
3const fs = require('fs').promises;
4...

我们刚刚导入了fs模块,该模块使用Promises而不是Callbacks。现在,我们需要对saveToFile()进行一些更改,这样它就可以使用Promises

在文本编辑器中,对 saveToFile() 函数进行以下更改,以删除回复:

 1[label todos/index.js]
 2...
 3saveToFile() {
 4    let fileContents = 'Title,Completed\n';
 5    this.todos.forEach((todo) => {
 6        fileContents += `${todo.title},${todo.completed}\n`
 7    });
 8
 9    return fs.writeFile('todos.csv', fileContents);
10}
11...

第一种不同之处在于,我们的函数不再接受任何论点. 对于 Promises,我们不需要回调函数. 第二种变化涉及到文件是如何写的。

保存并关闭index.js

现在让我们调整我们的测试,以便它与 Promises 一起工作. 打开 index.test.js:

1nano index.test.js

將「saveToFile()」測試變更為此:

 1[label todos/index.js]
 2...
 3describe("saveToFile()", function() {
 4    it("should save a single TODO", function() {
 5        let todos = new Todos();
 6        todos.add("save a CSV");
 7        return todos.saveToFile().then(() => {
 8            assert.strictEqual(fs.existsSync('todos.csv'), true);
 9            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
10            let content = fs.readFileSync("todos.csv").toString();
11            assert.strictEqual(content, expectedFileContents);
12        });
13    });
14});

我们需要做的第一个更改是从它的参数中删除完成( )的回调,如果Mocha通过了完成( )的参数,它需要被调用,或者它会引发这样的错误:

11) saveToFile()
2       should save a single TODO:
3     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)
4      at listOnTimeout (internal/timers.js:536:17)
5      at processTimers (internal/timers.js:480:7)

在测试 Promises 时,不要在 it() 中包含 done() 回调。

要测试我们的承诺,我们需要将我们的声明代码放到then()函数中,请注意,我们在测试中返回这个承诺,当Promise被拒绝时,我们没有catch()函数来捕捉。

我们返回承诺,以便在然后( )函数中抛出的任何错误都被泡沫到它( )函数。如果错误没有泡沫,Mocha将不会失败测试案例。当测试承诺时,您需要在被测试的承诺上使用回报

我们也忽略了‘catch()’条款,因为Mocha可以检测到承诺被拒绝时,如果被拒绝,它会自动失败。

现在我们有我们的测试,保存和退出文件,然后运行Mocha与npm测试,并确认我们得到一个成功的结果:

 1[secondary_label Output]
 2...
 3integrated test
 4    ✓ should be able to add and complete TODOs
 5
 6  complete()
 7    ✓ should fail if there are no TODOs
 8
 9  saveToFile()
10    ✓ should save a single TODO
11
12  3 passing (18ms)

我们已经更改了我们的代码和测试以使用 Promises,现在我们知道它确实有效,但最近的非同步模式使用async/await关键字,所以我们不必创建多个then()函数来处理成功的结果。

async / 等待

async / Wait 关键字使与 Promises 工作变得不那么简单。一旦我们将一个函数与 async 关键字定义为非同步,我们可以通过 Wait 关键字在该函数中获得任何未来的结果。

我们可以简化我们的saveToFile()测试,这是基于async/await的承诺。 在您的文本编辑器中,对index.test.js中的saveToFile()测试进行以下微小的编辑:

 1[label todos/index.test.js]
 2...
 3describe("saveToFile()", function() {
 4    it("should save a single TODO", async function() {
 5        let todos = new Todos();
 6        todos.add("save a CSV");
 7        await todos.saveToFile();
 8
 9        assert.strictEqual(fs.existsSync('todos.csv'), true);
10        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
11        let content = fs.readFileSync("todos.csv").toString();
12        assert.strictEqual(content, expectedFileContents);
13    });
14});

第一個變化是「it()」函數使用的函數現在在定義時具有「async」關鍵字,這使我們能夠在其內部使用「Wait」關鍵字。

第二个变化是在我们调用saveToFile()时发现的。在调用之前使用等待关键字.现在Node.js知道在继续测试之前等到这个函数得到解决。

我们的函数代码现在更容易阅读,因为我们将在then()函数中的代码移动到it()函数的体内。

 1[secondary_label Output]
 2...
 3integrated test
 4    ✓ should be able to add and complete TODOs
 5
 6  complete()
 7    ✓ should fail if there are no TODOs
 8
 9  saveToFile()
10    ✓ should save a single TODO
11
12  3 passing (30ms)

现在我们可以使用三种非同步范式中的任何一个适当地测试非同步函数。

我们已经用Mocha测试了大量的同步和非同步代码,接下来,让我们深入一些Mocha提供的其他功能,以改进我们的测试体验,特别是可以如何改变测试环境。

步骤5 – 使用胡子来改善测试案例

Hooks 是 Mocha 的一种有用的功能,它允许我们在测试之前和之后配置环境,我们通常会在描述() 函数块中添加 hooks,因为它们包含某些测试案例的设置和下载逻辑。

Mocha提供了我们可以在我们的测试中使用的四个钩子:

  • 之前: 在第一次测试开始前运行一次。
  • 之前Each: 每次测试之前运行一次.
  • 之后: 在最后一次测试结束后运行一次.
  • 之后Each: 每次测试结束后运行一次。

当我们测试一个函数或特性多次时,锁定会非常有用,因为它们允许我们将测试的设置代码(如创建todos对象)与测试的声明代码分开。

要查看的值,让我们在我们的saveToFile()测试块中添加更多测试。

虽然我们已经确认我们可以将我们的全部项目保存到一个文件中,但我们只保存了一个项目。 此外,该项目没有被标记为完成。

首先,讓我們添加第二個測試來確認我們的檔案在完成 TODO 項目時是否正確保存。

1nano index.test.js

更改最后的测试为以下:

 1[label todos/index.test.js]
 2...
 3describe("saveToFile()", function () {
 4    it("should save a single TODO", async function () {
 5        let todos = new Todos();
 6        todos.add("save a CSV");
 7        await todos.saveToFile();
 8
 9        assert.strictEqual(fs.existsSync('todos.csv'), true);
10        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
11        let content = fs.readFileSync("todos.csv").toString();
12        assert.strictEqual(content, expectedFileContents);
13    });
14
15    it("should save a single TODO that's completed", async function () {
16        let todos = new Todos();
17        todos.add("save a CSV");
18        todos.complete("save a CSV");
19        await todos.saveToFile();
20
21        assert.strictEqual(fs.existsSync('todos.csv'), true);
22        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
23        let content = fs.readFileSync("todos.csv").toString();
24        assert.strictEqual(content, expectedFileContents);
25    });
26});

关键差异在于,我们称之为完整()函数,然后称之为saveToFile(),而我们的期望FileContents现在为完成列的值具有而不是

保存和退出文件。

让我们运行我们的新测试,以及所有其他测试,用npm测试:

1npm test

这将给出如下:

 1[secondary_label Output]
 2...
 3integrated test
 4    ✓ should be able to add and complete TODOs
 5
 6  complete()
 7    ✓ should fail if there are no TODOs
 8
 9  saveToFile()
10    ✓ should save a single TODO
11    ✓ should save a single TODO that's completed
12
13  4 passing (26ms)

它按预期运作,然而,还有改进的余地。他们都必须在测试开始时实时化一个Todos对象。随着我们添加更多的测试案例,这很快就会变得重复性和记忆浪费。此外,每次我们运行测试,它会创建一个文件。这可能会被一个不熟悉模块的人误导为真实的输出。如果我们在测试后清理我们的输出文件,这将是件好事。

让我们使用测试链接进行这些改进。我们将使用beforeEach()链接来设置我们所有项目的 _test fixture。测试固定图是测试中使用的任何一致状态。在我们的情况下,我们的测试固定图是新的todos对象,其中已经添加了一个 TODO项。

index.test.js中,对saveToFile()的最后一次测试进行以下更改:

 1[label todos/index.test.js]
 2...
 3describe("saveToFile()", function () {
 4    beforeEach(function () {
 5        this.todos = new Todos();
 6        this.todos.add("save a CSV");
 7    });
 8
 9    afterEach(function () {
10        if (fs.existsSync("todos.csv")) {
11            fs.unlinkSync("todos.csv");
12        }
13    });
14
15    it("should save a single TODO without error", async function () {
16        await this.todos.saveToFile();
17
18        assert.strictEqual(fs.existsSync("todos.csv"), true);
19        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
20        let content = fs.readFileSync("todos.csv").toString();
21        assert.strictEqual(content, expectedFileContents);
22    });
23
24    it("should save a single TODO that's completed", async function () {
25        this.todos.complete("save a CSV");
26        await this.todos.saveToFile();
27
28        assert.strictEqual(fs.existsSync('todos.csv'), true);
29        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
30        let content = fs.readFileSync("todos.csv").toString();
31        assert.strictEqual(content, expectedFileContents);
32    });
33});

让我们把我们所做的所有更改分解下来,我们将一个beforeEach()块添加到测试块中:

1[label todos/index.test.js]
2...
3beforeEach(function () {
4    this.todos = new Todos();
5    this.todos.add("save a CSV");
6});
7...

这些代码的两行创建一个新的Todos对象,这将在我们的每个测试中可用。在Mocha中,在beforeEach()中的this对象指在it()中的同一个this对象。

这种强大的背景共享,就是为什么我们可以快速创建适用于我们两个测试的测试组件的原因。

然后我们在afterEach()函数中清理我们的 CSV 文件:

1[label todos/index.test.js]
2...
3afterEach(function () {
4    if (fs.existsSync("todos.csv")) {
5        fs.unlinkSync("todos.csv");
6    }
7});
8...

如果我们的测试失败了,那么它可能没有创建一个文件,这就是为什么我们在使用unlinkSync()函数删除它之前检查该文件是否存在。

剩余的更改将先前在it()函数中创建的todos引用转换为this.todos在Mocha环境中可用。

现在,让我们运行这个文件来确认我们的测试仍在工作. 输入您的终端中的npm 测试,以获取:

 1[secondary_label Output]
 2...
 3integrated test
 4    ✓ should be able to add and complete TODOs
 5
 6  complete()
 7    ✓ should fail if there are no TODOs
 8
 9  saveToFile()
10    ✓ should save a single TODO without error
11    ✓ should save a single TODO that's completed
12
13  4 passing (20ms)

结果是相同的,作为一个好处,我们稍微减少了对saveToFile()函数的新测试的设置时间,并找到了一种解决剩余的CSV文件的解决方案。

结论

在本教程中,您编写了一个 Node.js 模块来管理 TODO 项目,并使用 Node.js REPL 手动测试代码,然后创建了一个测试文件,并使用 Mocha 框架来运行自动测试。 使用肯定模块,您可以验证您的代码是否有效。

凭借这种理解,挑战自己为你正在创建的新 Node.js 模块撰写测试,你能否思考你的函数的输入和输出,并在写代码之前写下你的测试?

如果您想了解有关 Mocha 测试框架的更多信息,请参阅 官方 Mocha 文档. 如果您想继续学习 Node.js,您可以返回 如何在 Node.js 系列中编码页面。

Published At
Categories with 技术
comments powered by Disqus