Nodejs的測試和測試驅動開發
測試是保證軟體質量必不可少的一環。測試有很多形式:手動、自動、單元測試等等。這裡我們只聊使用Mocha這個框架在Nodejs中實現單元測試。單元測試是測試等重要組成,這樣的測試只對於一個方法,這樣的一小段程式碼,實施有針對的測試。
這裡會逐步深入的講解單元測試。首先是最簡單的單元測試,沒有外部依賴,只有簡單的輸入。接著是實用Sino框架實現stub等有依賴的測試。最後講解如何單元測試非同步程式碼。
安裝Mocha 和Chai
安裝Mocha:
npm install mocha -g
Mocha和其他的javascript單元測試框架,如:jasmine和QUnit不同,他沒有assertion庫。但是,Mocha允許你實用你自己的。最流行的Assertion庫有should.js、expect.js和Chai,當然Nodejs內建的也可以使用。這裡我們用Chai。
首先建立一個package.json並安裝Chai:
touch package.json
echo {} > package.json
npm install chai --save-dev
Chai包含三種assertion方式:should方式、expect方式和assert方式。個人喜歡expect式的,所以下面就使用這個方式了。
第一個Test
第一個例子,我們用測試驅動開發(TDD)的方式建立一個CartSummary
的建構函式,這個函式會用來計算購物車的商品總數。測試驅動開發就是在實現功能之前先寫單元測試,這樣來驅動你設計可以與測試相適應的程式碼。
測試驅動開發的步驟:
- 寫一個測試,並且這個測試會失敗。
- 寫最少的程式碼來使整個測試可以通過。
- 重複。
來看程式碼:
// 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
方法是用來建立一組測試的,並且可以給這一組測試一個描述。一個測試就用一個it方法。it方法的第一個引數是一個描述。第二個引數是一個包含一個或者多個assertion的方法。
執行測試只需要在專案的根目錄執行命令列:mocha tests --recursive --watch
。recursive指明會找到根目錄下的子目錄的測試程式碼並執行。watch則表示Mocha
會監視原始碼和測試程式碼的更改,每次更改之後重新測試。
我們測試不過,因為還沒有完成功能程式碼。新增程式碼:
// src/part1/cart-summary.js
function CartSummary() {}
CartSummary.prototype.getSubtotal = function() {
return 0;
};
module.exports = CartSummary;
測試就可以通過了:
下一個測試:
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);
});
這個測試時失敗的。。。
下面就來修改程式碼,讓測試通過:
// 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
假設我們現在需要給CartSummary新增getTax方法。最終的使用看起來是這樣的:
var cartSummary = new CartSummary([ /* ... */ ]);
cartSummary.getTax('NY', function() {
// executed when the tax API request has finished
});
getTax
方法會使用量外的一個tax模組,包含一個calculate
的方法。雖然我們還沒有實現tax模組,但是我們還是可以完成getTax
的測試。該怎麼做呢?
首先,安裝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
}
};
建立完成tax.calculate之後就可以使用Sinon的魔法了。用Sinon給出一個tax.calculate的零時實現。這個零時的實現就是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();
});
});
});
上面已經使用Sinon建立stub
方法了。這裡再細講一下。使用sinon.stub方法建立Stub:
var stub = sinon.stub(object,'method', func);
給object新增一個名稱為method(第二個引數)的方法,方法體的實現在第三個引數中給出。
上例中使用的方法體:
function(subtotal, state, done) {
setTimeout(function() {
done({
amount: 30
});
}, 0);
}
setTimeout
方法是用來模擬真實環境的,在實際使用的時候肯定會有一個非同步的網路請求來請求tax服務。方法體的替換在beforeEach
裡,這些程式碼會在測試開始之前執行。在所有測試完成之後呼叫afterEach
,並把tax.calculate
恢復到原來的模樣。
上面的例子也展示瞭如何測試非同步程式碼。在it
方法中指明一個引數(上例使用的是done
)。Mocha會傳入一個方法,並等待非同步程式碼返回再結束測試。當然,這個等待是由超時時間的,一般是2000毫秒。如果非同步程式碼的測試,沒有按照上面的方法寫的話,那麼所有的測試都會通過。
Sinon的”間諜”
Sinon的間諜(spy)是用來完成另外一種替身測試的(test double),它可以用來記錄方法呼叫。包括方法的呼叫次數、呼叫的時候的引數是什麼樣的以及是否丟擲異常。下面就是更新後的測試:
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();
});
});
在測試中添加了兩個expect。getCall用來獲取tax.calculate的第一次呼叫的第一個引數值,第二個getCall用來獲取tax.calculate的第一次呼叫的第二個引數。主要可以用來檢測被測試方法的引數是否正確。
總結
在本文中探討了如何在Node中使用Mocha以及Chai和Sinon實現單元測試。希望各位喜歡。