单元测试又称模块测试(Unit Testing),是对程序最小模块进行正确检验的测试工作,通常由开发人员完成。单元测试是保证模块正确性,提高程序可用性与健壮的重要手段。在Node.js中,单元测试使用较广泛的是使用Mocha测试框架。
1. Mocha测试框架
Mocha可以用于Node.js或浏览器端的JavaScript测试,支持多种assert断言库,同时支持同步和异步测试,也可以将测试结果导出。在Mocha测试框架中,除了要使用Mocha模块外,还要使用一些辅助模块,各模块功能如下。
1.1 Mocha测试中使用的模块
Mocha模块
Mocha是一个简单、可扩展的用于Node.js和JavaScript的单元测试框架。在Mocha的测试框架中,一般还要结合其它几个测试工具。
Should模块
Node本身提供了assert断言模块,但Should提供了更强大的表述性、可读性,在BDD测试中Should。
Supertest模块
在Web开发中,HTTP访问是必不可少的。Supertest模块提供了非常简单的HTTP请求与链式写法。
1.2 TDD测试与BDD测试
在单元测试中,有两类测试方式:TDD测试、BDD测试。
TDD(Test Driven Development)测试驱动开发。表示在开发功能代码之前,首先编写单元测试用例代码,由测试代码确定产品编写的代码。TDD是敏捷开发方法的核心实践,同样也适用于其他开发方法和过程。
BDD(Behavior Driven Development)形为驱动开发。行为驱动开发同样敏捷开发中应用的技术,它更注重软件项目中的开发者、QA和非技术人员及其它相关人员间的协作。
Mocha默认使用BDD测试,本文所有测试用例都是基于BDD测试。要使用TDD测试需要增加tdd参数:
mocha -u tdd test.js
2. Mocha单元测试
Mocha单元测试时,需要配合Should和Supertest模块。单元测试通常在开发过程中进行,所以我们可以在开发模式中安装这些模块:
npm install mocha should supertest --save-dev
在Node.js众多的npm模块中,单元测试是模块稳定性及质量保证的体现。在广泛使用的模块中,一般都会有一个test或类似的单元测试目录。Mocha默认会识别test目录,可以在命令行中执行mocha命令来进行单元测试:
$ mocha (node) child_process: options.customFds option is deprecated. Use options.stdio instead. 0 passing (4ms)
我们没有编写测试代码,mocha没有找到测试代码,所以没有进行任何操作。
2.1 Mocha单元测试示例
在test目录下编写如下代码,并保存为index.js文件:
require('should')
describe('Array', function() {
describe('#indexOf()', function() {
it('当指定值不存在时,则返回-1', function() {
[1,2,3].indexOf(5).should.equal(-1);
[1,2,3].indexOf(0).should.equal(-1);
});
});
});
执行mocha命令输出如下:
$ mocha
Array
#indexOf()
✓ 当指定值不存在时,则返回-1
1 passing (33ms)
也可以使用Node.js内置的assert模块:
var assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function () {
it('当指定值不存在时,则返回-1', function () {
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
});
});
});
输出如下:
$ mocha
Array
#indexOf()
✓ 当指定值不存在时,则返回-1
1 passing (29ms)
2.2 测试单元与测试用例
在上面的示例中,每个describe是一个测试套件,我们可以将其理解为一个测试单元。而每个it表示一个测试用例,一个describe可包括一个或多个it。
describe(moduleName, testDetails)
describe是可嵌套的,mocha测试单元。
moduleName是测试模块名,这个命名需要根据具体的测试单元设置,只要能让相关人员看明白即可,此命名是对测试单元的描述。如,上例中的#indexOf()。testDetails表示具体的测试方式,其中可以包含嵌套的describe或包含it块的测试用例。
it(info, function)
info表示测试描述信息,即测试说明(名称),该描述信息的结构是自外向里的形式描述测试模块。function是测试用的测试方法,该方法表示测试的执行过程
在前面的测试中,我们分别使用以下两组测试用例:
// should.js断言 [1,2,3].indexOf(5).should.equal(-1); [1,2,3].indexOf(0).should.equal(-1); // assert断言 assert.equal(-1, [1,2,3].indexOf(5)); assert.equal(-1, [1,2,3].indexOf(0));
这两组断言目的相同,只是描述方式不同。在这其中我,我们可以测试一个或多个测试用例。
2.3 测试形为管理
在单元测试中,有时我们需要在测试开始前或测试完成进行一些处理。Mocha提供了before()、after()、beforeEach()、afterEach()四个函数。before()和after()分别会在一个describe中的的所有测试用例开始之前和之后执行;而beforeEach()和afterEach()会在一个describe中的每测试用例之前和之后执行。
describe('behavior', function() {
before(function() {
// 在测试单元所有测试用例之前执行
});
after(function() {
// 在测试单元所有测试用例之后执行
});
beforeEach(function() {
// 在测试单元的每个测试用例之前执行
});
afterEach(function() {
// 在测试单元的每个测试用例之后执行
});
// 一些测试用例
});
2.4 测试用例管理
only选择执行
在项目测试中,有时我们只需要测试几个模块或用例,这时我们可以使用only()方法。describe和it都可以调用only()方法。
在下面示例中,上层的describe依然会被执行,但内层两个同级的describe只有被only选择的才会执行:
describe('Array', function() {
describe.only('#indexOf()不存在测试', function() {
it.only('如果不存在则返回 -1 ', function() {
[1, 2, 3].indexOf(5).should.equal(-1)
});
});
describe('#indexOf()存在测试', function() {
it('如果存在则返回索引序号', function() {
[1, 2, 3].indexOf(2).should.equal(-1)
});
});
});
上例执行结果如下:
Array
#indexOf()不存在测试
✓ 如果不存在则返回 -1
1 passing (13ms)
在it测试用例中,只有被only选择的用例才会执行:
describe('Array', function() {
describe('#indexOf()', function() {
it.only('如果不存在则返回 -1 ', function() {
[1, 2, 3].indexOf(5).should.equal(-1)
});
it('如果存在则返回索引序号', function() {
[1, 2, 3].indexOf(2).should.equal(-1)
});
});
});
上例执行结果如下:
Array
#indexOf()
✓ 如果不存在则返回 -1
1 passing (15ms)
skip跳过执行
对于不需要执行的测试单元或用例,可以使用skip方法将其跳过:
describe('Array', function() {
describe('#indexOf()', function() {
it('如果不存在则返回 -1 ', function() {
[1, 2, 3].indexOf(5).should.equal(-1)
});
it.skip('如果存在则返回索引序号', function() {
[1, 2, 3].indexOf(2).should.equal(-1)
});
});
});
3. Mocha使用示例
接下来是在项目测试过程中,一些具体使用的测试方法。
3.1 Mocha测试用例
我们有如下一个模块:
// add.js
function add(x, y) {
return x + y;
}
module.exports = add;
一般来说,在编写单元测试时,测试脚本要与项目模块一一对应。模块中的所有的方法都要在测试用例中测试(与测试覆盖率有关),测试脚本的的命名与模块名类似但以.test.js或.spec.js结尾。
如,我们对上面的add.js编写add.test.js测试脚本如下:
var add = require('../add.js');
require('should')
describe('add Test', function() {
it('1 加 1 应该等于 2', function() {
add(1, 1).should.equal(2);
});
});
接下来,我们可运行mocha命令测试所有的模块,或运行 mocha test/add.test.js来测试一个或多个模块。输出如下:
add Test
✓ 1 加 1 应该等于 2
1 passing (16ms)
3.2 Mocha异步测试
Node.js中由于其异步I/O机制大多数操作都是异步处理的,Mocha支持异步I/O的测试,只需要在其结尾添加回调函数即可。
下面是一个文件读取异步I/O的测试示例:
fs = require('fs');
describe('文件读取测试', function(){
describe('#readFile()', function(){
it('读取 test.txt文件', function(done){
fs.readFile('test.txt', function(err){
if (err) throw err;
done();
});
})
})
})
在异步测试中,Mocha通常使用命名为done()的回调函数,在一个it单元测试中只能有一个done()回调函数,多于一个时Mocha会抛出错误。
3.3 Mocha正常与异常值测试
在测试中通常会有正常值、异常值和边界值的测试,我们可以在Mocha进行这些测试以保证测试的覆盖率提高代码质量。
下面我们使用supertest模块模拟了用户名为空及用户名已存在时的用户注册场景,在这两种情况下用户注册都不应该成功:
var request = require('supertest');
require('should');
var password = 'a password';
var loginname = 'a exist username';
describe('登录', function() {
it('用户名为空时不能注册', function(done) {
request.post('/signup').send({
loginname: '', password: password
}).expect(200, function(err, res) {
should.not.exist(err);
res.text.should.containEql('用户名或密码不能为空');
done();
});
});
it('用户已经存在时不能注册', function(done) {
request.post('/signup').send({
loginname: loginname, password: password
}).expect(200, function(err, res) {
should.not.exist(err);
res.text.should.containEql('用户已经存在');
done();
});
});
});
3.4 结合Cookie与Session的测试
在HTTP开发,我们总是需要在Cookie开保持会话状态。不可避免的,在单元测试我们可需要结合Cookie进行测试,这种情况下通常需要在所使用Web框架的开发模式中设置相关会话值,然后Supertest模块进行测试。
在Express框架中,可以通过中间件来设置会话状态。
app.use(function(req, res, next) {
if (cprocess.env.NODE_ENV == "development" && req.cookies['testUser']) {
var testUser = JSON.parse(req.cookies['testUser']);
req.session.user = new User(testUser);
next(); }
next();
});
