本文介紹一下nodejs中常見的單元測試包及其使用
一、nodejs單元測試包簡介
- nodejs中最負盛名的單元測試框架是mocha,據(jù)官方資料,它已經(jīng)被超過10萬個npm包所依賴。其擁有豐富的,可配置和可擴展的測試特性。mocha默認使用nodejs內(nèi)置的斷言庫assert,但更好的選擇是使用第三方的斷言庫,根據(jù)單元測試和業(yè)務的需要。
- chia第三方斷言庫,支持各種斷言風格: expect,assert,should,詳見其官方文檔。
- sinon用于測試stubs和mocks。
- rewire用于重寫包引入機制, 使用得我們可以測試module中的私有方法。
- supertest用于測試web項目,模擬request請求。
三、mocha
mocha的使用非常簡單,官方文檔上有詳細的說明,但此處還是做一些簡單的說明。
- 安裝(推薦安裝成全局的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"
}
}
- 一個簡單的測試用例(來自官網(wǎng))
創(chuàng)建一個名為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));
});
});
});
命令行中執(zhí)行:
peachcat@peachcat:~/nodejs $ mocha
Array
#indexOf()
? should return -1 when the value is not present
1 passing (34ms)
- 一個異步調(diào)用的測試例子(來自官方)
異步調(diào)用完成之后需要調(diào)用mocha提供的callback,以完成此測試:
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(done);
});
});
});
- Promise:除了直接調(diào)用mocha的callback之外,還可以直接返回一個promise
const assert = require('assert');
it('should complete this test', function () {
return new Promise(function (resolve, reject) {
assert.ok(true);
resolve();
});
});
- 和generator一起使用,generator既沒有callback,也沒有返回promise,所以mocha無法直接處理,但我們可以使用co來執(zhí)行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");
});
});
});
- 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);
- equal和eql
equal: 使用"==="比較兩個值是否相等,當值為引用類型時,這只能比較引用的地址是否相等,若兩個對象內(nèi)容相同,但引用地址不同,即為兩個對象時,使用equal時,斷言會失敗。
expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' });
使用deep可以對比兩個對象的內(nèi)容,如:
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 ]);
- 異常斷言
使用".throw"可以斷言會拋出異常的方法,但是直接調(diào)用方法時,會拋出異常,導到測試直接失敗,所以需要對會拋出異常的方法進行包裝。比如:
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中被調(diào)用的。
generator方法中拋出異常: generator方法無法在expect中被執(zhí)行,即使使用了包裝方法也無法調(diào)用,因此generator檢測拋出異常時,可以使用下面的方式,使用co調(diào)用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中的幾種使用過的情況做下說明:
- stub一個方法,下面的代碼,將返回一個stub方法替代object中的"method"方法
var stub = sinon.stub(object, "method");
- stub一個方法,并且在使用指定參數(shù)被調(diào)用時,返回指定的數(shù)據(jù)。
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
- 使用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
}));
});
- spies、stub和mock的區(qū)別
sinon中提供了幾種有用的輔助測試功能,spies,stub和mock,下面說明這3種方式的意義和適合場景。
| 定義 | 適合場景 | |
|---|---|---|
| spies | spy是一個方法,測試時它會記錄下每一次此方法被調(diào)用時的參數(shù),返回值,或者拋出的異常。spy方法可以一個匿名方法,或者它可以包裝一個已存在的方法。 | 在測試callback和了解某個特定的方法在整個測試中是怎么被使用的是非常有用的。spy也可以包裝一個已有方法,同樣可以統(tǒng)計其調(diào)用情況,例子。見表下方的spy例子。 |
| stub | stub是一個預先編寫的方法,用于替代某個被測試的方法。stub的使用場景如下 | 1.單元測試中控制一個方法,強制其走到預置的代碼路徑;包括強制一個方法拋出異常以測試出錯的情況。2.避免一個方法被直接調(diào)用(比如:邏輯太復雜,和本次測試無關的方法;調(diào)用耗時太長的方法等)。 |
| mock | mock,類似于stub,同樣是一個預置的方法,同樣用于在測試中替代某個被測試的方法。與stub不同的是,mock方法在測試中必須被調(diào)用,否則測試用例失敗。 | 1. mock應該只被用于有單元測試的方法上。如果你想控制你的單元測試是怎么被使用,并且在方法被真實調(diào)用之前,請使用mock。2. mock有內(nèi)置的斷言,可能讓你的測試失敗。如果你不用在某個特殊調(diào)用上使用斷言,就沒必要使用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); //確定此方法做為回調(diào)方法被調(diào)用了。
}
五、rewire
nodejs模塊中,有許多私有方法,即未通過module.exports導出的方法,可以通過rewire這個npm包提供的方法rewire替換require,就有辦法可以訪問這些私有方法,從而對其做單元測試。例如:
- 在模塊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);
};
- 使用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);
});
});