NodeJS 测试:从 0 到 90
NodeJS 测试覆盖率:
最近,我负责一个基于 NodeJS 的应用程序。在这个过程中,我不得不学习许多新东西,其中之一就是测试。一开始,我写不了多少代码,更不用说测试它了。那时,我的测试覆盖率是 0%。一段时间后,特别是当我看到 npm 上优秀库的测试覆盖率达到 100% 时,我下定决心要学习 NodeJS 测试。目前,应用程序的测试覆盖率是 90% 以上。这就是我如何从 0 到 90 的。要实现剩下的 10% 我还有很长的路要走,我将在未来讨论这个问题。
前言
对于开发人员来说,测试的重要性毋庸置疑。你经常听到,“没有测试,就没有发布”,或者“没有测试,就没有重构”。然而,在实践中,总是存在某种测试问题。例如:
- 没有编写测试用例:通常,开发人员认为编写测试用例是浪费时间,或者他们可能不知道如何编写测试用例。
- 混乱的测试用例:测试用例可能写得随意,没有标准化,没有覆盖率,也没有集成。
- 编写测试用例可能比不编写更好:这些类型的测试用例通常不可读,不可维护,不可信,不可重复,不能独立运行(非常依赖某些环境或条件),或者不能执行(通常只在开发期间执行,以后不会执行,或者执行非常慢,或者没有人愿意执行)。
一个好的测试
如果你和一千个人交谈,你会听到至少两千种测试概念和方法。这使得很难定义什么是好的测试。随着我的团队对测试承担更多责任,我们形成了一个简单的理念。它包括以下原则:
- 覆盖率:75%+
衡量一个好测试最重要的指标是测试了多少代码(其覆盖率)。75% 是最低标准。这个标准对 Java 来说基本可行,但不适用于 NodeJS。JavaScript 是一种弱类型动态语言,没有编译阶段。这意味着许多错误只能在应用程序运行时才能发现。因此,我们需要更高的覆盖率。最好能达到 100%,但目前我的个人标准是 90% 以上。
- 可重复执行
每个测试用例都应该能够在任何环境中重复执行并产生相同的结果。只有这样,你才能真正信任你的测试并用它来发现实际的 bug。这也是集成测试的最低要求。
- 独立性
一个测试用例只测试代码的一个方面,例如一个分支,并且不高度依赖某些环境或条件。
- 可读、可维护、可信
- 快快快
无论是单个测试用例还是集成测试,都必须确保测试能够足够快地执行。
测试什么
测试什么这个问题主要根据实际需求、业务、成本、语言等因素来回答。然而,也有一些共性。单元测试原则(该链接为中文)提供了参考原则,但我在这里不会进一步讨论。
如何测试
这是一个非常广泛的问题。在这篇文章中,我将只根据自己的观点讨论 NodeJS 测试工具和方法。
概述
框架
NodeJS 测试框架有很多。目前,使用最广泛的是 Mocha。在本文中,我将详细描述 Mocha 并简要介绍其他几个框架。除非另有说明,本文中的所有示例都基于 Mocha。
Mocha
一个简单、灵活、有趣的 JavaScript 测试框架,适用于 node.js 和浏览器。
Mocha 是一个功能广泛的 JavaScript 测试框架。它可以在 Node.js 或浏览器上运行,并支持 BDD 和 TDD 测试。
快速入门
安装
npm install -g mocha
编写一个简单的测试用例
var assert = require('chai').assert;describe('Array', function() { describe('#indexOf()', function () { it('should return -1 when the value is not present', function () { assert.equal(-1, [1,2,3].indexOf(5)); assert.equal(-1, [1,2,3].indexOf(0)); }); }); });
Run
$ mocha
输出结果
1 test complete (1ms)
用途
断言
Mocha 允许你使用任何你想要的断言库,包括:
- should.js 本文档中展示的 BDD 风格
- expect.js expect() 风格断言
- chai expect()、assert() 和 should 风格断言
- better-assert C 风格的自文档 assert()
- unexpected 可扩展的 BDD 断言工具包
钩子 (Hooks)
Mocha 提供了钩子函数,例如 before()
、after()
、beforeEach()
和 afterEach()
。这些用于设置测试前条件和清理测试,示例如下:
专属测试或跳过测试
专属测试允许只测试指定的测试集或用例。对于专属测试,你只需在测试集或用例前添加 .only()
,如下所示:
跳过测试类似于 JUnit 的 @Ignore。它们用于跳过或忽略指定的测试集或用例。为此,只需在测试集或用例前添加 .skip()
,如下所示:
describe('Array', function(){ describe.skip('#indexOf()', function(){ ... }) })
编辑器插件
除了使用 Mocha 提供的命令行,你还可以使用编辑器插件(此链接无效)来运行测试用例。目前支持以下插件:
- TextMate
- JetBrains
- Wallaby.js
- Emacs
让我们以 JetBrains 为例。JetBrains 为其 IDE 套件(IntelliJ IDEA、WebStorm 等)提供了 NodeJS 支持。它可以直接运行或调试 Mocha 测试用例。基本过程:
- 安装 NodeJS 插件(如果未安装,则必须已安装 IntelliJ IDEA 或 WebStorm):前往 Preferences > Plugins,找到并安装名为 NodeJS 的插件。
- 添加 Mocha 测试。前往 Edit Configuration 并添加 Mocha,如下所示:
运行或调试测试。此插件允许你运行或调试目录、文件、测试集和测试用例。运行测试,如下所示:
其他
下面,我将列出其他几个基于 NodeJS 的测试框架或工具。它们可用于在 NodeJS 或你的浏览器中测试 JavaScript 代码。我将不再详细讨论它们,因此要了解更多信息,请参阅官方文档。
Jasmine
一个行为驱动开发 JavaScript 测试框架
Jasmine 是一个行为驱动开发框架,用于测试 JavaScript 代码。它不依赖于任何其他 JavaScript 框架。它不需要 DOM。而且它具有清晰、明确的语法,因此你可以轻松编写测试。
要了解更多信息,请参阅官方文档。
Karma Runner
为开发人员带来高效的测试环境
Karma 的主要目标是为开发人员带来高效的测试环境。这个环境是他们不必设置大量配置,而是开发人员可以编写代码并从他们的测试中获得即时反馈的地方。要了解更多信息,请参阅官方文档。工具
Mocha 提供了一个基本的测试框架,但在某些情况下,你可能还需要使用其他辅助工具。下面,我提供一个常用工具列表。
SuperTest
SuperTest 为 HTTP 测试提供高级抽象。这大大简化了基于 HTTP 的测试。
此模块的动机是为 HTTP 测试提供高级抽象,同时仍然允许你使用 super-agent 提供的低级 API。
安装
$ npm install supertest --save-dev
使用示例:
- 简单的 HTTP 请求
var request = require('supertest'); describe('GET /user', function() { it('respond with json', function(done) { request(app) .get('/user') .set('Accept', 'application/json') .expect('Content-Type', /json/) .expect(200, done); }); });
- 上传文件
request(app) .post('/') .field('name', 'my awesome avatar') .attach('avatar', 'test/fixtures/homeboy.jpg') // ..
- 修改响应头和正文
describe('GET /user', function() { it('user.name should be an case-insensitive match for "tobi"', function(done) { request(app) .get('/user') .set('Accept', 'application/json') .expect(function(res) { res.body.id = 'some fixed id'; res.body.name = res.body.name.toUpperCase(); }) .expect(200, { id: 'some fixed id', name: 'TOBI' }, done); }); });
要了解更多信息,请参阅文档。
代码覆盖率是软件测试中使用的度量。它描述了程序源代码中被测试的部分和程度。这个比例就是代码覆盖率:这个指标有四个维度:
- 行覆盖率:每行是否被执行
- 函数覆盖率:每个函数是否被调用
- 分支覆盖率:每个 if 代码块是否被执行
- 语句覆盖率:每个语句是否被执行
Istanbul 是最流行的 JavaScript 代码覆盖率工具。
快速入门
安装
$ npm install -g istanbul
Run
最简单的方法
$ cd /path/to/your/source/root $ istanbul cover test.js Output operation results: .. test/app/util/result.test.js should static create should be success should be static success should be error should be static error 299 passing (13s) [mochawesome] Report saved to /opt/source/node_modules/.mochawesome-reports/index.html =============================== Coverage summary =============================== Statements : 92.9% ( 1505/1620 ) Branches : 85.42% ( 410/480 ) Functions : 94.33% ( 133/141 ) Lines : 93.01% ( 1504/1617 ) ================================================================================ Done
此命令还将生成一个 coverage 子目录,其中 *coverage.json* 文件包含原始覆盖率数据。文件 coverage/lcov-report 是一个覆盖率报告,你可以在浏览器中打开,如下所示:
Istanbul
调式
两种常见的测试模式是 TDD 和 BDD。
TDD(测试驱动开发)
TDD 是敏捷开发中使用的核心实践和技术。它也是一种设计方法论。TDD 的原则如下:在开发功能代码之前,编写单元测试用例代码。此测试代码将决定需要编写哪些产品代码。TDD 的基本思想是测试可以驱动整个开发过程。然而,TDD 不仅仅涉及测试工作。它还涉及需求分析、设计和质量控制量化。总体过程如下:
我们将以一个小的阶乘程序为例。首先,编写测试用例,如下所示。现在,当你运行它时,它肯定会报错。这是因为要测试的程序尚未编写。
var assert = require('assert'), factorial = require('../index'); suite('Test', function (){ suite('#factorial()', function (){ test('equals 1 for sets of zero length', function (){ assert.equal(1, factorial(0)); }); test('equals 1 for sets of length one', function (){ assert.equal(1, factorial(1)); }); test('equals 2 for sets of length two', function (){ assert.equal(2, factorial(2)); }); test('equals 6 for sets of length three', function (){ assert.equal(6, factorial(3)); }); }); }); Start to write the factorial logic, as shown below. module.exports = function (n) { if (n < 0) return NaN; if (n === 0) return 1; return n * factorial(n - 1); };
现在,运行测试用例,看看程序是否通过测试。如果程序通过所有测试,则表明开发完成。如果未通过所有测试用例,则修复或修改被测逻辑,直到程序通过所有测试。
BDD(行为驱动开发)
BDD 是一种敏捷软件开发技术。它鼓励开发人员、QA 人员、非技术人员和商业参与者在软件项目中协作。这个过程从用户需求分析开始,并强调系统行为。它最显著的特点是使用书面的行为描述或规范来驱动软件开发。从外观上看,行为和规范描述与测试非常相似。然而,它们之间存在显著差异。
我们将继续使用上面介绍的阶乘来演示 BDD 模式测试。
从上面的例子中,我们可以看到 BDD 和 TDD 测试用例最大的区别在于措辞。BDD 测试用例读起来像普通的句子。因此,BDD 测试用例可以用作促进开发人员、QA 人员、非技术人员和商业参与者之间协作的工具。如果开发人员能够更流畅地阅读测试用例,他们自然能够编写更好、更全面的测试用例。
TDD 与 BDD
本质上和目的上,TDD 和 BDD 是相同的。它们只在实现上有所不同,并且安排了不同的讨论以改进整体敏捷开发系统。TDD 迭代地重复验证以确保敏捷开发。然而,它没有明确规定如何根据设计生成测试用例并确保测试用例的质量。另一方面,BDD 提倡使用清晰自然的语言来描述系统行为。这正好弥补了测试用例(系统行为)的准确性。
基本上所有基于 NodeJS 的库和应用程序都选择 BDD,尽管我不明白确切的原因。
断言
目前有几个流行的断言库:
- should.js 本文档中展示的 BDD 风格
- expect.js expect() 风格断言
- chai expect()、assert() 和 should 风格断言
- better-assert C 风格的自文档 assert()
- unexpected 可扩展的 BDD 断言工具包
除了风格上的细微差异,这些库或多或少都是相同的。你可以选择最适合你的偏好或应用程序需求的库。
通过这些测试框架运行我的 NodeJS 测试,我能够获得 90% 以上的测试覆盖率。要实现超过 90% 的测试覆盖率,请留意我未来不久的文章。