測(cè)試是保證軟件質(zhì)量必不可少的一環(huán)。測(cè)試有很多形式:手動(dòng)、自動(dòng)、單元測(cè)試等等。這里我們只聊使用Mocha這個(gè)框架在Nodejs中實(shí)現(xiàn)單元測(cè)試。單元測(cè)試是測(cè)試等重要組成,這樣的測(cè)試只對(duì)于一個(gè)方法,這樣的一小段代碼,實(shí)施有針對(duì)的測(cè)試。
這里會(huì)逐步深入的講解單元測(cè)試。首先是最簡(jiǎn)單的單元測(cè)試,沒(méi)有外部依賴,只有簡(jiǎn)單的輸入。接著是實(shí)用Sino框架實(shí)現(xiàn)stub等有依賴的測(cè)試。最后講解如何單元測(cè)試異步代碼。
安裝Mocha 和Chai
安裝Mocha:
npm install mocha -g
Mocha和其他的javascript單元測(cè)試框架,如:jasmine和QUnit不同,他沒(méi)有assertion庫(kù)。但是,Mocha允許你實(shí)用你自己的。最流行的Assertion庫(kù)有should.js、expect.js和Chai,當(dāng)然Nodejs內(nèi)置的也可以使用。這里我們用Chai。
首先創(chuàng)建一個(gè)package.json并安裝Chai:
touch package.json
echo {} > package.json
npm install chai --save-dev
Chai包含三種assertion方式:should方式、expect方式和assert方式。個(gè)人喜歡expect式的,所以下面就使用這個(gè)方式了。
第一個(gè)Test
第一個(gè)例子,我們用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)的方式創(chuàng)建一個(gè)CartSummary
的構(gòu)造函數(shù),這個(gè)函數(shù)會(huì)用來(lái)計(jì)算購(gòu)物車的商品總數(shù)。測(cè)試驅(qū)動(dòng)開(kāi)發(fā)就是在實(shí)現(xiàn)功能之前先寫單元測(cè)試,這樣來(lái)驅(qū)動(dòng)你設(shè)計(jì)可以與測(cè)試相適應(yīng)的代碼。
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的步驟:
- 寫一個(gè)測(cè)試,并且這個(gè)測(cè)試會(huì)失敗。
- 寫最少的代碼來(lái)使整個(gè)測(cè)試可以通過(guò)。
- 重復(fù)。
來(lái)看代碼:
// tests/part1/cart-summary-test.js
var chai = require('chai');
var expect = chai.expect; // we are using the "expect" style of Chai
var CartSummary = require('./../../src/part1/cart-summary');
describe('CartSummary', function() {
it('getSubtotal() should return 0 if no items are passed in', function() {
var cartSummary = new CartSummary([]);
expect(cartSummary.getSubtotal()).to.equal(0);
});
});
describe
方法是用來(lái)創(chuàng)建一組測(cè)試的,并且可以給這一組測(cè)試一個(gè)描述。一個(gè)測(cè)試就用一個(gè)it方法。it方法的第一個(gè)參數(shù)是一個(gè)描述。第二個(gè)參數(shù)是一個(gè)包含一個(gè)或者多個(gè)assertion的方法。
運(yùn)行測(cè)試只需要在項(xiàng)目的根目錄運(yùn)行命令行:mocha tests --recursive --watch
。recursive指明會(huì)找到根目錄下的子目錄的測(cè)試代碼并運(yùn)行。watch則表示Mocha
會(huì)監(jiān)視源代碼和測(cè)試代碼的更改,每次更改之后重新測(cè)試。
我們測(cè)試不過(guò),因?yàn)檫€沒(méi)有完成功能代碼。添加代碼:
// src/part1/cart-summary.js
function CartSummary() {}
CartSummary.prototype.getSubtotal = function() {
return 0;
};
module.exports = CartSummary;
測(cè)試就可以通過(guò)了:
下一個(gè)測(cè)試:
it('getSubtotal() should return the sum of the price * quantity for all items', function() {
var cartSummary = new CartSummary([{
id: 1,
quantity: 4,
price: 50
}, {
id: 2,
quantity: 2,
price: 30
}, {
id: 3,
quantity: 1,
price: 40
}]);
expect(cartSummary.getSubtotal()).to.equal(300);
});
這個(gè)測(cè)試時(shí)失敗的。。。
下面就來(lái)修改代碼,讓測(cè)試通過(guò):
// src/part1/cart-summary.js
function CartSummary(items) {
this._items = items;
}
CartSummary.prototype.getSubtotal = function() {
if (this._items.length) {
return this._items.reduce(function(subtotal, item) {
return subtotal += (item.quantity * item.price);
}, 0);
}
return 0;
};
Stub和Sinon
假設(shè)我們現(xiàn)在需要給CartSummary添加getTax方法。最終的使用看起來(lái)是這樣的:
var cartSummary = new CartSummary([ /* ... */ ]);
cartSummary.getTax('NY', function() {
// executed when the tax API request has finished
});
getTax
方法會(huì)使用量外的一個(gè)tax模塊,包含一個(gè)calculate
的方法。雖然我們還沒(méi)有實(shí)現(xiàn)tax模塊,但是我們還是可以完成getTax
的測(cè)試。該怎么做呢?
首先,安裝Sinon:
npm install --save-dev sinon
安裝Sinon之后,我們就可以給出tax.calculate的定義了:
// src/part1/tax.js
module.exports = {
calculate: function(subtotal, state, done) {
// implemented later or in parallel by our coworker
}
};
創(chuàng)建完成tax.calculate之后就可以使用Sinon的魔法了。用Sinon給出一個(gè)tax.calculate的零時(shí)實(shí)現(xiàn)。這個(gè)零時(shí)的實(shí)現(xiàn)就是Stub(也叫做樁)。代碼:
// tests/part1/cart-summary-test.js
// ...
var sinon = require('sinon');
var tax = require('./../../src/part1/tax');
describe('getTax()', function() {
beforeEach(function() {
sinon.stub(tax, 'calculate', function(subtotal, state, done) {
setTimeout(function() {
done({
amount: 30
});
}, 0);
});
});
afterEach(function() {
tax.calculate.restore();
});
it('get Tax() should execute the callback function with the tax amount', function(done) {
var cartSummary = new CartSummary([{
id: 1,
quantity: 4,
price: 50
}, {
id: 2,
quantity: 2,
price: 30
}, {
id: 3,
quantity: 1,
price: 40
}]);
cartSummary.getTax('NY', function(taxAmount) {
expect(taxAmount).to.equal(30);
done();
});
});
});
上面已經(jīng)使用Sinon創(chuàng)建stub
方法了。這里再細(xì)講一下。使用sinon.stub方法創(chuàng)建Stub:
var stub = sinon.stub(object,'method', func);
給object添加一個(gè)名稱為method(第二個(gè)參數(shù))的方法,方法體的實(shí)現(xiàn)在第三個(gè)參數(shù)中給出。
上例中使用的方法體:
function(subtotal, state, done) {
setTimeout(function() {
done({
amount: 30
});
}, 0);
}
setTimeout
方法是用來(lái)模擬真實(shí)環(huán)境的,在實(shí)際使用的時(shí)候肯定會(huì)有一個(gè)異步的網(wǎng)絡(luò)請(qǐng)求來(lái)請(qǐng)求tax服務(wù)。方法體的替換在beforeEach
里,這些代碼會(huì)在測(cè)試開(kāi)始之前執(zhí)行。在所有測(cè)試完成之后調(diào)用afterEach
,并把tax.calculate
恢復(fù)到原來(lái)的模樣。
上面的例子也展示了如何測(cè)試異步代碼。在it
方法中指明一個(gè)參數(shù)(上例使用的是done
)。Mocha會(huì)傳入一個(gè)方法,并等待異步代碼返回再結(jié)束測(cè)試。當(dāng)然,這個(gè)等待是由超時(shí)時(shí)間的,一般是2000毫秒。如果異步代碼的測(cè)試,沒(méi)有按照上面的方法寫的話,那么所有的測(cè)試都會(huì)通過(guò)。
Sinon的"間諜"
Sinon的間諜(spy)是用來(lái)完成另外一種替身測(cè)試的(test double),它可以用來(lái)記錄方法調(diào)用。包括方法的調(diào)用次數(shù)、調(diào)用的時(shí)候的參數(shù)是什么樣的以及是否拋出異常。下面就是更新后的測(cè)試:
it('getTax() should execute the callback function with the tax amount', function(done) {
var cartSummary = new CartSummary([
{
id: 1,
quantity: 4,
price: 50
},
{
id: 2,
quantity: 2,
price: 30
},
{
id: 3,
quantity: 1,
price: 40
}
]);
cartSummary.getTax('NY', function(taxAmount) {
expect(taxAmount).to.equal(30);
expect(tax.calculate.getCall(0).args[0]).to.equal(300);
expect(tax.calculate.getCall(0).args[1]).to.equal('NY');
done();
});
});
在測(cè)試中添加了兩個(gè)expect。getCall用來(lái)獲取tax.calculate的第一次調(diào)用的第一個(gè)參數(shù)值,第二個(gè)getCall用來(lái)獲取tax.calculate的第一次調(diào)用的第二個(gè)參數(shù)。主要可以用來(lái)檢測(cè)被測(cè)試方法的參數(shù)是否正確。
總結(jié)
在本文中探討了如何在Node中使用Mocha以及Chai和Sinon實(shí)現(xiàn)單元測(cè)試。希望各位喜歡。
原文地址:https://www.codementor.io/nodejs/tutorial/unit-testing-nodejs-tdd-mocha-sinon