内容


学习 Node.js,第 9 单元:单元测试

了解 Node.js 单元测试

Comments

代码无错是确保应用和业务流程顺利运行的关键。测试和静态分析(运行一个工具来识别代码中的潜在错误)只是为确保代码质量而可以采取的众多操作中的两种操作。测试和静态分析主要好在哪里?这两个过程都可以完全自动化。在我的 Node.js 学习路径的第 9 单元中,学习如何自动执行测试和静态分析,以便构建无错的 Node.js 应用程序。同时还将向您介绍 Node 中一些最流行的测试自动化工具:

  • Mocha:一种测试框架
  • Chai:一种断言库
  • Sinon:一种测试替身库
  • Istanbul:一种代码覆盖率库
  • ESLint:一种静态分析实用程序

所有这些工具都纳入 NPM 生态系统,可以接入到您的 npm 生命周期中。

获取代码

您在此学习路径中跟随其中示例一起进行操作所需的代码可在我的 GitHub 存储库中找到。

测试自动化概述

就本单元而言,测试是指根据程序要求检查代码以确保两者匹配的行为。自动化测试是指让计算机为您运行此检查而无需您手动操作的行为。

简言之,测试过程如下(须注意,我提倡测试驱动的开发):

  1. 编写测试用例,即受应用程序需求控制的代码。
  2. 编写用于实现应用程序需求的代码。这是受测试的代码
  3. 根据受测试的代码运行测试用例,直到所有测试用例通过为止。

自动化测试有几个关键优势:

  • 以代码形式编写测试用例可以提高代码质量,因为它减少了测试过程中出现人为错误的可能性。您可以预先编写测试用例,然后计算机可以根据您正在测试的代码反复运行这些用例。
  • 您还可以根据需要随时向测试套件(测试用例集合)添加新的测试用例。如果您忘记编写测试用例,可稍后将它添加到集合中。
  • 如果在编写测试用例时出错,可以调试测试用例本身,因为测试用例就是代码。从那时起,测试用例应该按照可预测的方式运行。

手动测试则与之相反:在手动测试中,每次更改内容时,都要记住运行所有的测试用例。这会导致更长的测试周期,容易发生人为错误,包括忘记运行测试。

在后面几节中,我将向您介绍本教程中用于自动化测试的工具。这些绝不是 Node 生态系统中唯一可用的测试工具。它们只是一些比较流行的工具,恰好也能很好协作。

Mocha:一种测试框架

测试框架是用于定义并提供以下几项内容的软件:

  • 测试 API,用于指定如何编写测试代码。
  • 测试发现,用于确定哪些 JavaScript 文件是测试代码。
  • 测试生命周期,用于定义在运行测试之前、期间和之后发生的事件。
  • 测试报告,用于记录运行测试时发生的情况。

Mocha 是最流行的 JavaScript 测试框架之一,您在开发期间很可能会遇到它。Jest 是另一个流行的 Node 测试框架。 对于本教程,我没有时间同时介绍这两个框架,所以我选择了 Mocha。

为了告知 Mocha 您的 JavaScript 代码是一个测试,可以使用 Mocha 测试 API 中提供的特殊关键字:

  • describe() 表示一个任意嵌套的测试用例分组(一个 Description() 可以包含其他 Description())。
  • it() 表示单个测试用例。

这两个函数都有两个参数:

  • 在测试报告中显示的描述。
  • 回调函数

我们将在本教程的后面再讨论这些内容。

用 Mocha 编写测试套件

最简单的测试套件只包含一个测试:

清单 1. 仅含一个测试的 Mocha 测试套件

const {describe} = require('mocha');

const assert = require('assert');

describe('Simple test suite:', function() {
    it('1 === 1 should be true', function() {
        assert(1 === 1);
    });
});

在上面的清单中,describe 已经在全局上下文中;因此,require('mocha') 是不必要的,但为了说明起见,我还是想把它包含在内。

以下是此测试的输出:

$ cd src/projects/IBM-Developer/Node.js/Course/Unit-9
$ ./node_modules/.bin/mocha test/example1.js


  Simple test suite:
    ✓ 1 === 1 should be true


  1 passing (5ms)

在本例中,我使用了 Node 的 assert 模块,但这不是最具表达性的断言库。幸运的是,Mocha 并不关心您使用哪个库,所以您可以自由选择最喜欢的库。

挑战问题 1 如何运行清单 1 中的测试?看看您能否明白我是如何实现的。本单元的结尾给出了答案。

Chai:一种断言库

Chai 是最流行的 JavaScript 测试断言库之一。它易于使用,适用于 Mocha,并提供了两种风格的断言:

  • Assert:assertEqual(1, 1)
  • BDD(行为驱动开发):expect(1 === 1).to.be.trueexpect(1).to.equal(1)

Chai 还允许您插入自己的断言库,但在本教程中不会讨论这个问题。

我喜欢 BDD 风格的断言,因为它们比 assert 风格的断言表达性更强。表达性断言使测试代码更易读,更易于维护。在本教程中,我们将使用 Chai 的 BDD 风格断言。

使用 Chai 的测试套件

让我们修改清单 1 中的测试套件以使用 Chai。

清单 2. 使用 Chai 的 BDD 风格断言库的 Mocha 测试套件

const {describe} = require('mocha');

const {expect} = require('chai');

describe('Simple test suite (with chai):', function() {
    it('1 === 1 should be true', function() {
        expect(1).to.equal(1);
    });
});

输出如下:

$ cd ~/src/projects/IBM-Developer/Node.js/Course/Unit-9
$ ./node_modules/.bin/mocha test/example2.js


  Simple test suite (with chai):
    ✓ 1 === 1 should be true


  1 passing (6ms)

清单 2 中的示例可能与第一个测试套件没有太大不同,但是使用 Chai 的 BDD 风格断言语法可以提高测试的可读性。

Sinon:一种测试替身库

测试替身是一个代码块,用于替换部分生产代码以便进行测试。如果不方便甚至不可能对生产代码运行测试用例,那么测试替身就非常有用。当生产代码需要连接到数据库时,或者您需要获得精确的系统时间时(您很快就会看到这一点),测试替身便能派上用场。

自行编写这样的代码可能面临出现技术问题的风险。幸运的是,Node 社区已经开发了几个用于测试替身的包。其中一个名为 Sinon 的库非常流行。您可能会碰到这个库,所以我们在这里了解一下。

使用间谍、存根和模拟对象进行测试

测试替身有几种类型:

  • 间谍 (spy) 用于包装一个真实函数,以便记录其相关信息,比如它被调用了多少次以及使用哪些参数来调用。
  • 伪造对象 (fake) 是一个间谍对象,它只会假装是一个真实函数,这样它就可以记录有关该函数的信息。
  • 模拟对象 (mock) 类似于一个伪造函数,只是使用了您所指定的期望值,例如函数被调用多少次以及使用哪些参数来调用。
  • 存根 (stub) 类似于间谍,只是用您所指定的行为替换了真实函数。

清单 3 展示了如何创建一个间谍。

清单 3. 窥探 console.log() 的 Sinon 间

const {describe, it} = require('mocha');

const {expect} = require('chai');

const sinon = require('sinon');

describe('When spying on console.log()', function() {
    it('console.log() should still be called', function() {
        let consoleLogSpy = sinon.spy(console, 'log');
        let message = 'You will see this line of output in the test report';
        console.log(message);
        expect(consoleLogSpy.calledWith(message)).to.be.true;
        consoleLogSpy.restore();
    });
});

输出如下所示:

$ cd ~/src/projects/IBM-Developer/Node.js/Course/Unit-9
$ ./node_modules/.bin/mocha test/example3.js


  When spying on console.log()
You will see this line of output in the test report
    ✓ console.log() should still be called


  1 passing (7ms)

须注意,测试报告中显示了 console.log 输出。这是因为间谍不会替换函数,而只是窥探函数。如果您希望调用真实函数,但需要进行相关断言,就可以使用间谍。您可以在清单 3 中看到这一点,其中的测试规定必须使用精确的 message 来调用 console.log()

如何编写存根

要用提供实现的函数替换 console.log(),可以使用存根或模拟对象。清单 4 显示了如何编写一个非常简单的存根。

清单 4. 用一个不执行任何操作的函数来替换 console.log() 的 Sinon 存根

const {describe, it} = require('mocha');

const {expect} = require('chai');

const sinon = require('sinon');

describe('When stubbing console.log()', function() {
    it('console.log() is replaced', function() {
        let consoleLogStub = sinon.stub(console, 'log');
        let message = 'You will NOT see this line of output in the test report';
        console.log(message);
        consoleLogStub.restore();
        expect(consoleLogStub.calledWith(message)).to.be.true;
    });
});

以下是这个示例的输出:

$ cd ~/src/projects/IBM-Developer/Node.js/Course/Unit-9
$ ./node_modules/.bin/mocha test/example4.js


  When stubbing console.log()
    ✓ console.log() is replaced and the stub is called instead


  1 passing (8ms)

须注意,在此示例中,该消息没有显示在测试报告中。这是因为用存根替换了真正的 console.log()。另须注意,必须在 expect() 断言调用之前调用 consoleLogStub.restore(),否则测试报告看上去将不正确。

挑战问题 2 为什么在调用任何断言逻辑之前必须在 Sinon 存根上调用 restore() 查看 Sinon 存根文档, 看看您是否可以找到原因。

模拟对象与存根非常相似,所以在这里,我就不再演示如何编写模拟对象了。我们将在本单元后面的内容中再次涉及到模拟对象和存根。

Istanbul:一种测试代码覆盖率库

代码覆盖率是一个代码质量度量,用于衡量在运行测试时(即,在 npm test 的单次调用期间,您很快就会看到),实际执行了多少受测试的潜在可执行代码。

在自动化测试中,您会希望尽可能多地运行可执行代码。运行的代码越多,发现的缺陷也就越多。因此,对于代码覆盖率,您想要争取达到 100%,这意味着在自动化测试时,所有可能的可执行代码行都在运行。

Istanbul 是用于在 JavaScript 中测试代码覆盖率的一个很受欢迎的库,并且有一个用于此库的 Node 模块。Istanbul 可在运行测试之前动态检测您的代码,然后在测试运行期间跟踪执行了多少代码。

在本单元后面的内容中,您将使用 Istanbul 的命令行界面 nyc

ESlint:一种插件式静态分析实用程序

linter 是一个用来分析代码中潜在错误的工具,有时称为静态代码分析

在代码上运行 linter 称为静态分析,这一技术可以非常方便地发现以下问题:

  • 未声明的变量
  • 未使用的变量或函数
  • 过长的源代码行
  • 格式错误的注释
  • 缺少的文档注释
  • 其他众多内容

您可能还记得单元 7 中的内容:npm 注册表包含许多用于静态分析的工具。对于本教程,我选择的工具是 ESLint。ESLint 会根据配置插件(配置)报告不同的问题,有些插件会比其他插件更宽容。除了 eslint:recommended 配置之外,我们还使用了可共享的eslint-config-google。可供选择的可共享配置有很多:搜索 npm 注册表,亲眼看一看。

在本单元的后面部分中,我将向您展示如何在 npm test 生命周期内,将 linter 配置为两个单独的 npm 脚本并运行。

设置示例项目

到目前为止,您已经大致了解了测试技术,以及在 npm 生命周期内可用于自动执行测试的一些工具。 在本节中,我们设置一个示例项目,并编写一些测试。这些示例都需要亲自动手操作,因此您应该按照后续部分中的说明安装所有包。同样,您应该严格遵循有关编写测试的说明,以便学习和研究测试代码。

项目设置

要设置示例项目和测试,需要执行以下操作:

  1. 初始化项目
  2. 安装以下包:
    • Mocha
    • Chai
    • Sinon
    • Istanbul CLI - nyc
  3. 配置 package.json

我们将一起执行这些步骤。一旦设置了示例项目,便可以开始编写测试代码。

第 1 步. 初始化项目

最简单的着手方法是打开终端窗口或命令提示符,导航到 Unit-9 目录,并输入以下命令:touch package.json。这将创建一个空白的 package.json 文件。

将空白的 package.json 替换为以下内容:

{
  "name": "logger",
  "version": "1.0.0",
  "main": "logger.js",
  "license": "Apache-2.0",
  "scripts": {
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/jstevenperry/node-modules"
  }
}

第 2 步. 安装测试包

接下来,您将安装每个测试包。

安装 Mocha

在终端窗口中,输入以下命令:npm i --save-dev mocha

输出如下所示:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm i --save-dev mocha
npm notice created a lockfile as package-lock.json. You should commit this file.
+ mocha@5.2.0
added 24 packages from 436 contributors and audited 31 packages in 1.739s
found 0 vulnerabilities

这将安装 Mocha 的最新版本,并将 mocha 保存在 package.json 的 devDependencies 节中。

安装 Chai

在终端窗口中,输入以下命令:npm i --save-dev chai

输出如下所示:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm i --save-dev chai
+ chai@4.1.2
added 7 packages from 20 contributors and audited 39 packages in 1.157s
found 0 vulnerabilities

这将安装 Chai 的最新版本,并将 chai 保存在 package.json 的 devDependencies 节中。

安装 Sinon

在终端窗口中,输入以下命令:npm i sinon

输出如下所示:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm i --save-dev sinon
+ sinon@6.1.3
added 11 packages from 317 contributors and audited 57 packages in 1.687s
found 0 vulnerabilities

这将安装 Sinon 的最新版本,并将 sinon 保存在 package.json 的 devDependencies 节中。

安装 Istanbul CLI

在终端窗口中,输入以下命令:npm i --save-dev nyc

输出如下所示:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm i --save-dev nyc
+ nyc@12.0.2
added 284 packages from 148 contributors and audited 2246 packages in 5.744s
found 0 vulnerabilities

这将安装 Istanbul 命令行接口(称为 nyc)的最新版本,并将 nyc 保存到 package.json 的 devDependencies 节中。

第 3 步. 配置 package.json

现在打开 package.json,并将以下内容添加到 scripts 元素中:

"test": "nyc mocha ./test"

这个脚本告诉 npm 调用 Istanbul CLI (nyc) 和 Mocha,后者将发现和运行位于 ./test 目录中的测试。

在执行进一步操作之前,确保您的 package.json 如下面的清单 5 所示。

清单 5. 安装了 Mocha、Chai、Sinon 和 Istanbul 的 package.json 以及 test 脚本

{
  "name": "logger",
  "version": "1.0.0",
  "main": "logger.js",
  "license": "Apache-2.0",
  "scripts": {
    "test": "nyc mocha ./test"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/jstevenperry/node-modules"
  },
  "devDependencies": {
    "chai": "^4.1.2",
    "mocha": "^5.2.0",
    "nyc": "^12.0.2",
    "sinon": "^6.1.3"
  }
}

如果您安装的包版本与清单 5 中的包版本不同,也不用担心。在撰写本文时,我已经安装了最新版本,但这些版本将会随着时间而更新。

编写测试代码

本部分将为本单元中的最后练习做好准备,您在此将完成 logger.js 的测试和实现代码。在实际编写代码之前,我建议您通读整个部分。这将有助于您较好地了解在进入 logger.jstest-logger.js 模块之前需要做些什么。开始编写代码时(很快就要开始了),您也可以回过头来参考本部分内容。

使用 Mocha 和 Chai 编写测试

在本例中,我们使用 Mocha 作为测试框架,Chai 作为支持 BDD 风格断言的断言库。

在清单 2 中,您已经发现用 Mocha 和 Chai 编写测试非常容易。您了解了如何通过在一个 describe() 函数调用中嵌套另一个 describe() 函数调用(并在其中再嵌套一个,以此类推),创建所需的测试套件分组。现在,您将使用 it() 函数编写一个测试用例,如下所示:

清单 6. test-logger.js 中的测试代码

.
.
describe('Module-level features:', function() {
    // TRACE
    describe('when log level isLevel.TRACE', function() {
        it('should have a priority order lower than Level.DEBUG', function() {
            expect(Level.TRACE.priority).to.be.lessThan(Level.DEBUG.priority);
        });
        it('should have outputString value of TRACE', function() {
            expect(Level.TRACE.outputString).to.equal('TRACE');
        });
    });
.
.

清单 6 中的嵌套测试套件包含两个测试用例:

  • 第一个测试用例可确保 Level.TRACE 优先级属性值低于 Level.DEBUG 的值。
  • 第二个测试用例可确保 outputString 属性是 TRACE

须注意,在断言 expect().to.be.lessThan()expect().to.be.equal() 中使用了函数链。函数链在 BDD 风格的断言中很常见,这可提高它们的可读性。只需查看函数链,就可以清楚地看到每个断言的作用。

运行清单 6 中的测试用例时,输出将如下所示(先不要尝试,否则您会遇到很多错误):

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm test
.
.
> logger@1.0.0 test /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-9
> nyc mocha ./test

.
.
  Module-level features:
    when log level isLevel.TRACE
      ✓ should have a priority order lower than Level.DEBUG
      ✓ should have outputString value of TRACE
.
.

我从整个测试套件中删除了一些输出,这只是为了显示清单 2 中所示部分的输出。

使用 Mocha 和 Sinon 编写测试

Sinon 是一个测试库,它允许您在测试中使用测试替身。在本部分,您将详细了解如何在 Mocha 测试中使用存根和模拟对象,以及这两者的相关示例。

编写存根

存根 函数是一个测试替身,它用您自己编写的自定义行为来替换某些函数的行为。当您想要用所选的值来替换对 Date.now() 的调用时,这特别方便。

存根是间谍的一种类型,但是存根会用您的函数来替换真实函数,而间谍则不会。间谍只是观察和返回报告,就像现实世界中的间谍一样。

打开 logger.js 模块,查看 log() 函数。您将看到以下内容:

01 function log(messageLogLevel, message, source, logFunction) {
 02   let computedMessage = null;
 03   if (messageLogLevel.priority >= logLevel.priority) {
 04     let now = Date.now();
 05     let outputString = now.toString() + ':' + messageLogLevel.outputString;
 06     computedMessage = outputString + ': ' + ((source) ? source + ': ' : '') +
 07       message;
 08     (logFunction) ? logFunction(computedMessage) : logMessage(computedMessage);
 09   }
 10   return computedMessage;
 11 }

测试用例全都关乎可预测性,记录器使用 Date.now()(第 4 行)作为 computedMessage(第 6 行)的一部分,这是实际记录的字符串(第 8 行)。

将存根用于时间戳

您可能已经知道,Date.now() 会返回自 Unix 纪元 以来的毫秒数,因此,它始终在变化。如何正确断言预期消息的值?

解决方案是去除真实的 Date.now() 函数,将其替换为返回所选值的函数,然后在断言中使用该值。

使用 Sinon 创建存根非常容易:

let dateStub = sinon.stub(Date, 'now').returns(1111111111);
.
// Use the stubbed version
.
dateStub.restore();

首先,调用 sinon.stub() 函数,传递要存根的对象(Date 类)和要存根的函数 (now())。然后,您需要向返回的存根(Sinon API 是流畅的)添加一个对 returns() 的调用,指示存根在被调用时返回 1111111111

此后(直到调用了 dateStub.restore()),每次调用 Date.Now() 时,都将替换为您的存根。将返回 1111111111 值,而不是 Date.now() 通常返回的运行毫秒值。

切记,存根将替换真实函数,因此,如果您希望 Date.now() 在测试运行后正确运行,就需要在存根上调用 restore() 函数。

编写模拟对象

模拟对象 函数是一个测试替身,它使用一组期望值替换某个真实函数。与存根不同,模拟对象不提供实现。

如果检查 logger.js 模块的 log() 函数,您会注意到最后一个参数是执行消息实际日志记录的函数。看一下帮助函数,您会注意到,它们也全都使用这个参数。

如果省略了这个参数,log() 将调用 logMessage() ,这是一个内部函数,委托给 sole e.log() 。对于测试,我们不希望将测试报告输出(默认情况下显示在控制台中)与程序输出混为一谈。我们会将每个 logger 帮助方法的最后一个参数替换为一个模拟对象。然后,我们可以验证它的调用次数是否与预期一致。

使用 Sinon 创建模拟对象非常容易:

01      describe('Default log level of INFO', function() {
 02          let mockLogFunction;
 03
 04          before(function() {
 05              // Make sure the default is correct
 06              logger.setLogLevel(logger.DEFAULT_LOG_LEVEL);
 07              mockLogFunction = sinon.mock().exactly(4);
 08          });
 09          after(function() {
 10              mockLogFunction.verify();
 11          });
 12          // TRACE
 13          it('should not log a TRACE level message and return null', function() {
 14              expect(logger.trace('BAR', 'foo()', mockLogFunction)).to.be.null;
 15          });

记住,测试框架的功能之一就是定义测试生命周期。您可以在上面的示例中看到这一点,其中 Mocha 的 before() 函数在组内的任何测试用例之前运行一次,其 after() 函数在所有测试用例运行之后运行一次。

模拟对象是在第 7 行中设置的,这个特定测试组中的函数期望被调用四次。

一旦测试用例都在 after() 中运行,就会调用 verify() 函数来验证是否满足了期望。如果未满足期望,将会抛出异常。

对于 Mocha ,这些测试示例已经足够。在开始编写代码之前,让我们来安装、配置和运行 linter。

安装 ESLint

在终端窗口中,输入以下命令:npm i --save-dev eslint eslint-config-google

输出如下所示:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm i --save-dev eslint eslint-config-google
+ eslint-config-google@0.9.1
+ eslint@5.2.0
added 119 packages from 146 contributors and audited 2491 packages in 7.696s
found 0 vulnerabilities

这将安装 ESLint 的最新版本和 Google 共享的 ESLint 配置。npm 还会将 eslinteslint-config-google 保存到 package.json 的 devDependations 节中,现在应该如下所示:

"devDependencies": {
    "babel-eslint": "^8.2.6",
    "chai": "^4.1.2",
    "eslint": "^5.2.0",
    "eslint-config-google": "^0.9.1",
    "mocha": "^5.2.0",
    "nyc": "^12.0.2",
    "sinon": "^6.1.4"
  }

配置并运行 linter

ESLint 的配置方法有许多,但我建议使用 package.json ,以便尽量减少必须在项目中来回拖动的元数据文件的数量。在这个例子中,我们对 ESLint 同时使用了 eslint:recommendedgoogle 配置。

首先,将下面的代码片段粘贴到 package.json 中的 devDependents 节后面。您需要在此节末尾添加一个逗号,以便使 JSON 格式正确:

"eslintConfig": {
    "extends": ["eslint:recommended", "google"],
    "env": {
        "node" : true
    },
    "parserOptions": {
      "ecmaVersion": 6
    },
    "rules" : {
      "max-len": [2, 120, 4, {"ignoreUrls": true}],
      "no-console": 0
    }
  },
  "eslintIgnore": [
    "node_modules"
  ]

extends 元素表示要使用的配置数组,在此情况下,是 eslint:recommendedgoogle

添加 env 元素和值为 truenode,便可以使用全局变量,如 requiremodule

我们将使用 ES6 语法 (parserOptions) 。还须注意,在 rules 下面,我让 max-len 更符合我的喜好(默认为 80 )。

我们当然也不希望对 node_modules 中的所有 JavaScript 代码进行静态分析,因为这会产生可怕的输出量。

其他脚本

接下来,将以下代码片段添加到 package.json 中的 scripts 元素:

"lint": "eslint .",
    "lint-fix": "npm run lint -- --fix",
    "pretest": "npm run lint"

第一个脚本 lint 实际上运行 linter (eslint) ,并告知其对当前目录中的所有内容进行静态分析。ESLint 还将对每个下级目录进行静态分析,您明确告诉它要忽略的目录除外(如上面所示,通过 eslintIgnore 元素)。

我创建了第二个脚本 lint-fix,允许 linter 自动修正常见代码 陷阱,比如,行中的多余空格,或者在单行注释后无空格。安装了这个脚本后,您只需要运行 npm run lint-fix,linter 将为您清理代码。(也许将来总有一天,当我注释掉一行代码时,我会记得在 // 之后添加一个空格,但这看起来并不好。)

最后,npm 内置的 pretest 脚本将确保您的代码在每次运行 npm test 时都会被执行静态分析。如果 pretest 脚本太烦人,可以将其删除,但是我喜欢将它放在那里,这样我就不会忘记每次更改代码时都对代码进行静态分析

关于目录结构的说明(保持目录整洁)

如果您询问 10 位开发人员他们将单元测试放在 Node 项目中的哪个目录,他们的回答可能不尽相同。就我个人而言,我喜欢使源代码尽可能接近 package.json ,且使用 最小 目录结构。我想要一个整洁且可维护的布局,这样就可以很容易地找到所需内容。

接下来的练习使用以下目录布局:

x:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ tree .
.
├── logger.js
├── solution
│   ├── exercise-1
│   │   ├── logger.js
│   │   └── test
│   │       └── test-logger.js
│   ├── logger.js
│   ├── package.json
│   └── test
│       └── test-logger.js
└── test
    ├── example1.js
    ├── example2.js
    ├── example3.js
    ├── example4.js
    └── test-logger.js

5 directories, 11 files

最低原则:如果贵公司已有标准,应遵循该标准。否则,只需采用一个可以使代码保持整洁的最小目录结构。

练习:单元测试 logger.js

您已经了解了如何用 Chai 和 Sinon 编写 Mocha 测试,以及如何运行 linter 。现在是时候编写一些代码了,您将自己编写代码。

为帮助您着手,我创建了以下框架版本:

  • 您首先要编写的测试模块,位于 ./test/test-logger.js
  • 实现代码,位于 logger.js

我推荐使用测试驱动开发 (TDD) ,所以务必首先编写所有测试代码,然后再运行 npm test

因为 linter 首先在 pretest 中运行,所以您先要解决所有静态分析错误, 然后测试用例才会运行 。如果一开始看到很多错误,不要灰心。这个练习的两个配置都相当挑剔,但最终结果会是一个超级整洁的代码。

一旦测试用例最终开始运行,第一个任务就是观察它们全部失败。 然后 (只能在此之后),应开始编写 logger.js 实现,直到测试通过为止。

在需要编写代码的框架中有一些 TODO: 注释。在看到 TODO: 注释的地方,应遵循相关说明。我还提供了一些参考代码块来帮助您。

祝您好运!

如果其他一切方法都不幸失败

如果您似乎无法让代码正常工作,并且您确实希望它能够工作,那么在 solution/package.json 中有一个名为 test-solution 的特殊脚本可运行解决方案代码。要运行此脚本:

  1. solution/package.json 复制到 Unit-9 目录(这将覆盖您迄今为止对 Unit-9/package.json 所做的任何更改)。
  2. 调用 npm 以运行脚本:npm run test-solution

您应会看到类似如下的输出:

Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ cp solution/package.json .
Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$ npm run test-solution

> logger@1.0.0 test-solution /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-9
> npm run pretest && nyc mocha ./solution/test


> logger@1.0.0 pretest /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-9
> npm run lint


> logger@1.0.0 lint /Users/sperry/home/development/projects/IBM-Developer/Node.js/Course/Unit-9
> eslint .



  Module-level features:
    when log level isLevel.TRACE
      ✓ should have a priority order lower than Level.DEBUG
      ✓ should have outputString value of TRACE
    Level.DEBUG
      ✓ should have a priority order less than Level.INFO
      ✓ should have an outputString value of DEBUG
    Level.INFO
      ✓ should have a priority order less than Level.WARN
      ✓ should have specific outputString values
    Level.WARN
      ✓ should have a priority order less than Level.ERROR
      ✓ should have an outputString value of WARN
    Level.ERROR
      ✓ should have a priority order less than Level.FATAL
      ✓ should have an outputString value of ERROR
    Level.FATAL
      ✓ should have a priority order less than Level.OFF
      ✓ should have an outputString value of FATAL
    Level.OFF
      ✓ should have an outputString value of OFF
    Default log level of INFO
      ✓ should not log a TRACE level message and return null
      ✓ should not log a DEBUG level message and return null
      ✓ should log an INFO level message
      ✓ should log a WARN level message
      ✓ should log an ERROR level message
      ✓ should log a FATAL level message

  Log Levels and the API:
    when current log Level=TRACE
      ✓ should output TRACE level message
      ✓ should output DEBUG level message
      ✓ should output INFO level message
      ✓ should output WARN level message
      ✓ should output ERROR level message
      ✓ should output FATAL level message
    when current log Level=DEBUG
      ✓ should not output TRACE level message
      ✓ should output DEBUG level message
      ✓ should output INFO level message
      ✓ should output WARN level message
      ✓ should output ERROR level message
      ✓ should output FATAL level message
    when current log Level=INFO
      ✓ should not output TRACE level message
      ✓ should not output DEBUG level message
      ✓ should output INFO level message
      ✓ should output WARN level message
      ✓ should output ERROR level message
      ✓ should output FATAL level message
    when current log Level=WARN
      ✓ should not output TRACE level message
      ✓ should not output DEBUG level message
      ✓ should not output INFO level message
      ✓ should output WARN level message
      ✓ should output ERROR level message
      ✓ should output FATAL level message
    when current log Level=ERROR
      ✓ should not output TRACE level message
      ✓ should not output DEBUG level message
      ✓ should not output INFO level message
      ✓ should not output WARN level message
      ✓ should output ERROR level message
      ✓ should output FATAL level message
    when current log Level=FATAL
      ✓ should not output TRACE level message
      ✓ should not output DEBUG level message
      ✓ should not output INFO level message
      ✓ should not output WARN level message
      ✓ should not output ERROR level message
      ✓ should output FATAL level message
    when current log Level=OFF
      ✓ should not output TRACE level message
      ✓ should not output DEBUG level message
      ✓ should not output INFO level message
      ✓ should not output WARN level message
      ✓ should not output ERROR level message
      ✓ should not output FATAL level message

  Code Coverage:
    ✓ should invoke logMessage() at least once so coverage is 100%


  62 passing (42ms)

-----------------|----------|----------|----------|----------|-------------------|
File             |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files        |      100 |      100 |      100 |      100 |                   |
 solution        |      100 |      100 |      100 |      100 |                   |
  logger.js      |      100 |      100 |      100 |      100 |                   |
 solution/test   |      100 |      100 |      100 |      100 |                   |
  test-logger.js |      100 |      100 |      100 |      100 |                   |
-----------------|----------|----------|----------|----------|-------------------|
Ix:~/src/projects/IBM-Developer/Node.js/Course/Unit-9 sperry$

结术语

本单元首先概要介绍了测试和静态分析。虽然,我向您展示了如何执行以下操作:

  • 使用 Chai 和 Sinon 编写 Mocha 测试
  • 接入 Istanbul 以获取代码覆盖率度量
  • 配置并运行 ESLint ,分析您的代码是否存在潜在错误和缺陷

本单元的最后是一个单独练习,即完成编写在框架记录器文件中找到的测试和实现代码。

在第 10 单元,您将学习如何使用 Winston ,这是 Node 生态系统中最流行的日志记录包之一。

挑战问题的答案

挑战问题 1:我是如何运行该测试 (example1.js) 的

首先,我安装了 Mocha:

npm i --save-dev mocha

除非您全局安装了某个包(使用 -g 标志),否则所有包都是相对于当前目录进行安装的。包含可执行程序的包(如 Mocha\)始终安装在 ./node_modules/.bin 中。 Mocha 可执行文件 (mocha) 以文件列表或测试所在目录作为参数。

源代码可以在 GitHub 中获得,当您将源代码克隆到计算机上时,第 9 单元的源代码的相对路径是 ./IBM-Developer/Node.js/Course/Unit-9 。在我的电脑上,完整路径是 ~/src/projects/IBM-Developer/Node.js/Course/Unit-9

要运行单个测试,只需传递包含该测试的 JavaScript 模块的名称,如下所示:

./node_modules/.bin/mocha test/example1.js

挑战问题 2:为什么在调用任何断言逻辑之前必须在 Sinon 存根上调用 restore()

当 Sinon 创建一个测试替身(如存根)时,Sinon 会将原始函数替换为您在 V8引擎中运行的 JavaScript 代码 中指定的函数实现。如果不调用 restore(),那么在 V8 引擎的此实例中,系统会继续将存根用于对存根函数的所有调用。

视频

观看视频:学习 Node.js, 第 9 单元:单元测试
观看视频:学习 Node.js, 第 9 单元:单元测试

测试您的理解情况

判断题

  1. 函数存根可用于替换您指定了行为的真实函数。
  2. linter 用于根据源代码注释或 "静态分析" 来运行自动单元测试。
  3. 代码覆盖率工具用于确保您对模块中的每个函数都进行单元测试。

查看答案

选择最佳答案

  1. 关于 package.json 中的 pretest 脚本,以下哪句陈述是正确的?

    A. 此脚本在套件中的每个单元测试之前运行。

    B. 此脚本在 npmtest 脚本之前运行。

    C. 此脚本从不运行,只是用于记录 test 脚本。

    D. 此脚本在 test 脚本之前和之后都会运行。

  2. 关于使用像 Mocha 这样的测试框架,以下哪一项 不是 其优点?

    A. 它可发现要运行的测试。

    B. 它可定义用于编写测试的 API。

    C. 它可报告测试结果。

    D. 它可自动运行您在配置中插入的代码覆盖率工具。

  3. 以下哪几项 不是 执行自动化测试的理由?

    A. 自动化测试很容易出现人为错误,因为它们运行过于频繁,以至于测试结果失去了所有意义。

    B. 自动化测试缩短了开发生命周期,因为测试可以由计算机运行,而无需手动运行。

    C. 自动化测试可以由 npm 自动生成,并插入到 npm 生命周期的 autotest 阶段。

    D. 自动化测试减少了人为错误的可能性,因为可以对测试代码本身进行调试,然后以一致的可预测方式运行。

    E. 上述选项都不是。

查看答案

填空

  1. 本章讨论的两大主题是 _______________ 和 _______________ 。
  2. 在编写 Mocha 测试时,可以使用 _____________() 函数对测试用例进行分组,并可使用 ___________() 函数编写测试用例。
  3. 如果您只需将真实函数替换为所提供的自定义实现(不关心是否检查期望),那么应该使用 __________________ 类型的测试替身。
  4. 在本单元中,您使用了 _____________ 来创建测试替身,使用 ____________ 测试代码覆盖率,使用 _____________ 进行断言,并使用 _____________ 作为测试框架。

查看答案

编程练习“加餐”

  1. 向记录器 API 中添加一个名为 severe() 的新帮助函数。新函数应具备对应的 Level,其 priority 介于 ERRORFATAL 之间,并且 outputStringSEVERE )。首先将单元测试添加到 ./test/test-logger.js 中,然后编写实现,直到测试通过为止。
  2. test-logger.js 中的 Sinon 存根 dateNowStub 替换为伪造对象。它是否仍以相同方式运行?为什么不能将 dateNowStub 替换为间谍?

查看答案

查看答案

判断题答案

  1. :通过对真实函数进行存根,您可以将它替换为自己的函数。如果使用 Date.now() 这样的函数(您希望控制其输出),那么这样做很有用。
  2. :Linter 是一种工具,用于对代码进行静态分析,以发现潜在的错误和违反编程约定的行为,这些都是由您使用的配置提供的。
  3. :像 Istanbul's CLI (nyc) 这样的代码覆盖率工具会动态检测您的代码。然后运行单元测试,并报告实际执行的可执行代码所占比例。

选择题答案

  1. B:当您运行 npm test 时,pretest 脚本在 test 脚本 之前 运行。
  2. D:虽然您可以将代码覆盖率工具与测试框架 配合使用,但您需要在测试框架外部执行这样的操作。该框架并未提供代码覆盖率工具。
  3. B DA 是无稽之谈,npm 生命周期根本不存在 autotest 阶段。

填空题答案

  1. 测试静态分析
  2. describeit
  3. 存根 (但如果您确实关心设置和是否检查期望 - 例如,函数被调用了多少次,那么应该使用模拟对象。)
  4. SinonIstanbul (nyc)ChaiMocha

编程练习“加餐”答案

  1. 参阅 ./solution/exercise-1/logger.js 了解修改后的 logger.js 代码,并参阅 ./solution/exercise-1/test/test-exercise-1.js 了解修改后的 ./test/test-logger.js 代码。
  2. 您的代码应类似如下:

test-logger.js 中:

.
.
// FORMERLY The Date.now() stub - NOW A FAKE!!!
let dateNowFake = null;

// Do this before every test
beforeEach(function() {
    dateNowFake = sinon.fake.returns(1111111111);
    sinon.replace(Date, 'now', dateNowFake);
});
// Do this after every test
afterEach(function() {
    sinon.restore();
});
.
.

您不能将 dateNowStub 替换为间谍,因为您不能使用间谍来替换函数。

参考资源

本文翻译自:Learn Node.js, Unit 9: Unit testing(2019-01-16)


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=1066040
ArticleTitle=学习 Node.js,第 9 单元:单元测试
publish-date=06132019