使用mocha和chai做nodejs单元测试

本文介绍一下nodejs中常见的单元测试包及其使用

一、nodejs单元测试包简介

  1. nodejs中最负盛名的单元测试框架是mocha,据官方资料,它已经被超过10万个npm包所依赖。其拥有丰富的,可配置和可扩展的测试特性。mocha默认使用nodejs内置的断言库assert,但更好的选择是使用第三方的断言库,根据单元测试和业务的需要。
  2. chia第三方断言库,支持各种断言风格: expect,assert,should,详见其官方文档。
  3. sinon用于测试stubs和mocks。
  4. rewire用于重写包引入机制, 使用得我们可以测试module中的私有方法。
  5. supertest用于测试web项目,模拟request请求。

三、mocha

mocha的使用非常简单,官方文档上有详细的说明,但此处还是做一些简单的说明。

  1. 安装(推荐安装成全局的npm)
$ npm install --global mocha

或者将依赖写入了package.json中的devDependencies,例如:

{
  "devDependencies": {
    "mocha": "^3.2.0",
    "chai": "^3.5.0",
    "co": "^4.6.0",
    "rewire": "^2.5.2",
    "sinon": "^1.17.7"
  }
}
  1. 一个简单的测试用例(来自官网)
    创建一个名为test的目录,在目录中新建test.js(~/nodejs/test/test.js)文件,输入以下代码:
var assert = require('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(4));
    });
  });
});

命令行中执行:

peachcat@peachcat:~/nodejs $ mocha


  Array
    #indexOf()
      ✓ should return -1 when the value is not present


  1 passing (34ms)
  1. 一个异步调用的测试例子(来自官方)
    异步调用完成之后需要调用mocha提供的callback,以完成此测试:
describe('User', function() {
  describe('#save()', function() {
    it('should save without error', function(done) {
      var user = new User('Luna');
      user.save(done);
    });
  });
});
  1. Promise:除了直接调用mocha的callback之外,还可以直接返回一个promise
const assert = require('assert');
                               
it('should complete this test', function () {
  return new Promise(function (resolve, reject) {
    assert.ok(true);           
    resolve();                 
  });
});
  1. 和generator一起使用,generator既没有callback,也没有返回promise,所以mocha无法直接处理,但我们可以使用co来执行generator,并且co返回的就是promise。例子:
'use strict';
var fs = require("fs") ;       
let co = require('co');        
var assert = require('assert');

function readFile(path){       
  return new Promise(function(resolve, reject){
    fs.readFile(path, "utf8", function(err, data){
      if(err){                 
        reject(err);           
      }else{
        resolve(data);         
      }
    });
  });
}   
    
describe("Generator", function(){
  it('test with co', function () {
    return co(function*(){     
      let txt = yield readFile("a.txt");
      assert.equal(txt, "file A\n");  
    });
  });
});
  1. mocha支持before(), after(), beforeEach(), and afterEach()这类方法,用于设置预置的测试条件和清理测试之后的资源。使用方式如下:
describe('hooks', function() {
  before(function() {
    // runs before all tests in this block
  });
  after(function() {
    // runs after all tests in this block
  });
  beforeEach(function() {
    // runs before each test in this block
  });
  afterEach(function() {
    // runs after each test in this block
  });
  // test cases
});

三、chia

chia支持多种写法: Should,Expect,Assert,这里主要使用的是expect方式,官方有个大概的例子:

var expect = chai.expect;

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(tea).to.have.property('flavors').with.lengthOf(3);
  1. equal和eql

equal: 使用"==="比较两个值是否相等,当值为引用类型时,这只能比较引用的地址是否相等,若两个对象内容相同,但引用地址不同,即为两个对象时,使用equal时,断言会失败。

expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' });

使用deep可以对比两个对象的内容,如:

expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' });

eql: 等价于deap.equal,如:

expect({ foo: 'bar' }).to.eql({ foo: 'bar' });
expect([ 1, 2, 3 ]).to.eql([ 1, 2, 3 ]);
  1. 异常断言

使用".throw"可以断言会抛出异常的方法,但是直接调用方法时,会抛出异常,导到测试直接失败,所以需要对会抛出异常的方法进行包装。比如:
m.js

module.exports.throwError = function(a){
  if( a > 100){                
    throw new ReferenceError('This is a bad function.');;
  } else {
    return a * 10;
  }
};

测试代码:

'use strict';

let expect = require('chai').expect;
let m = require("../m");

describe("Chia throw test", function(){
  it("should throw ReferenceError", function(){
    expect(m.throwError(101)).to.throw(ReferenceError);
  });
});

此测试用例无法通过:

peachcat@peachcat:~/nodejs/mocha $ mocha test/m.js 


  Chia throw test
    1) should throw ReferenceError


  0 passing (57ms)
  1 failing

  1) Chia throw test should throw ReferenceError:
     AssertionError: expected [Function] to throw ReferenceError
      at Context.<anonymous> (test/m.js:8:34)

将throwError 方法包装起来,修改之后的代码如下:

'use strict';

let expect = require('chai').expect;
let m = require("../m");

describe("Chia throw test", function(){
  it("should throw ReferenceError", function(){
    let warpper = function(){ m.throwError(101); }
    expect(warpper).to.throw(ReferenceError);
  });     
});

再次跑测试,顺利通过:

peachcat@peachcat:~/nodejs/mocha $ mocha test/m.js 


  Chia throw test
    ✓ should throw ReferenceError


  1 passing (51ms)

从上面的代码来看,会抛出异常的方法,是在expect中被调用的。

generator方法中抛出异常: generator方法无法在expect中被执行,即使使用了包装方法也无法调用,因此generator检测抛出异常时,可以使用下面的方式,使用co调用generator方法,在catch中获得异常,并对此异常进行断言。例子如下:

'use strict';
let co = require('co');        
let expect = require('chai').expect;

function* generatorMethod(a){  
  if (a > 10){                 
    throw new Error("Generator Error");
  } else {
    return a * 10;
  }
}   
    
describe("Generator", function(){
  it('test throw Error', function () {
    return co(function*(){     
      let results = yield generatorMethod(11);
    }).catch(function(err){
      expect(err.message).to.equal("Generator Error");
    });
  });
});

四、sinon

sinon可以做许多事情,比如: stub, mock,可以模拟ajax请求等等。这里我们主要使用了stub功用。下面就stub中的几种使用过的情况做下说明:

  1. stub一个方法,下面的代码,将返回一个stub方法替代object中的"method"方法
var stub = sinon.stub(object, "method");
  1. stub一个方法,并且在使用指定参数被调用时,返回指定的数据。
var stub = sinon.stub(object, "method");
stub.withArgs(42).returns(1);
stub.withArgs(1).throws("TypeError");

object.method(42);  // return 1
object.method(1);   // throws TypeError
  1. 使用sandbox隔离stub,nodejs中模块,在全局上是同一个对象,因此对某个模块进行了stub,后面的测试还需要使用此模块时会相互影响,因此可以使用sinon的sandbox功能,将stub进行隔离。例如:
describe('get_all_miss_jsons', function(){
  it("#getAllMissJsons", sinon.test(function(){
    //使用this.stub替代全局的sinon.stub
    this.stub(missJsonService, "fetch").returns(someObject); 

    //assert
  }));
});
  1. spies、stub和mock的区别
    sinon中提供了几种有用的辅助测试功能,spies,stub和mock,下面说明这3种方式的意义和适合场景。
定义 适合场景
spies spy是一个方法,测试时它会记录下每一次此方法被调用时的参数,返回值,或者抛出的异常。spy方法可以一个匿名方法,或者它可以包装一个已存在的方法。 在测试callback和了解某个特定的方法在整个测试中是怎么被使用的是非常有用的。spy也可以包装一个已有方法,同样可以统计其调用情况,例子。见表下方的spy例子。
stub stub是一个预先编写的方法,用于替代某个被测试的方法。stub的使用场景如下 1.单元测试中控制一个方法,强制其走到预置的代码路径;包括强制一个方法抛出异常以测试出错的情况。2.避免一个方法被直接调用(比如:逻辑太复杂,和本次测试无关的方法;调用耗时太长的方法等)。
mock mock,类似于stub,同样是一个预置的方法,同样用于在测试中替代某个被测试的方法。与stub不同的是,mock方法在测试中必须被调用,否则测试用例失败 1. mock应该只被用于有单元测试的方法上。如果你想控制你的单元测试是怎么被使用,并且在方法被真实调用之前,请使用mock。2. mock有内置的断言,可能让你的测试失败。如果你不用在某个特殊调用上使用断言,就没必要使用mock。另外,一个独单的测试中,不要使用超过1个以上的mock。

一个spy的例子:

"test should call subscribers on publish": function () {
    var callback = sinon.spy();    //假装一个callback
    PubSub.subscribe("message", callback);

    PubSub.publishSync("message");

    assertTrue(callback.called); //确定此方法做为回调方法被调用了。
}

五、rewire

nodejs模块中,有许多私有方法,即未通过module.exports导出的方法,可以通过rewire这个npm包提供的方法rewire替换require,就有办法可以访问这些私有方法,从而对其做单元测试。例如:

  1. 在模块m.js中,存在私有方法_add
'use strict';

function _add(a, b){
  return a + b;
};

module.exports.add = function(a, b){
  if (a > 100 ){
    a = a * 2;
  }
  return _add(a, b);
};
  1. 使用rewire测试此方法:
let rewire = require('rewire');
let m      = rewire("../m");
var expect = require('chai').expect;

describe("rewire", function(){
  it("test private method in module `m`" ,function(){
    let _add = m.__get__('_add');
    expect(_add(4, 3)).to.eq(7);
  }); 
});
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容