Mocha和Should、Supertest模块搭建Node.js单元测试环境

 2016年04月15日    581     声明


单元测试又称模块测试(Unit Testing),是对程序最小模块进行正确检验的测试工作,通常由开发人员完成。单元测试是保证模块正确性,提高程序可用性与健壮的重要手段。在Node.js中,单元测试使用较广泛的是使用Mocha测试框架。


  1. Mocha测试框架
  2. Mocha单元测试
  3. 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单元测试时,需要配合ShouldSupertest模块。单元测试通常在开发过程中进行,所以我们可以在开发模式中安装这些模块:

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()方法。describeit都可以调用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(); 
    }); 
  }); 
});


在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(); 
});