單元測試:Mocha、Chai 和 Sinon
17 Sep 2017單元測試(Unit Test)
什麼是 Unit Test?
以 function 為最小單位,驗證在特定情況下的 input 和 output 是否正確。
例如
// 給定輸入為 1 和 2
var result = add(1, 2);
// 預期得到輸出 3
if (result !== 3) {
throw new Error('Failed');
}
為什麼要做 Unit Test?
- 防止改 A 壞 B,避免不能跑的程式碼比能跑的還多
- 明確指出問題所在、告知正確的行為是什麼,減少猜測時間
以上這些目的都可以使用人工測試達到,但如果能靠程式自動且在極短時間內完成呢?這樣是不是很美好?使用以下的方法做 Unit Test 就可以這麼輕鬆愉快喔 :)
如何做 Unit Test?
- 使用 npm 等安裝 Mocha、Chai 和 Sinon 並 require。
- 可選擇環境為 Browser-based testing 或 Node.js testing。
- 撰寫測試程式。
- 使用 mocha 啟動測試。
- 檢視測試報告,確認通過或未通過的項目。
- 根據測試結果進行調整:對通過的程式碼做重構,或對未通過的程式碼除錯。
Mocha
Mocha 是一個 JavaScript 的測試框架,目的是用來管理測試的程式碼。
在這裡指定介面(Interface)為 BDD。若使用 BDD 則提供 describe()
、it()
、before()
、after()
、beforeEach()
與afterEach()
方法。
語法說明
describe()
:描述場景或圈出特定區塊,例如:標明測試的功能或 function。it()
:撰寫測試案例(Test Case)。before()
:在所有測試開始前會執行的程式碼區塊。after()
:在所有測試結束後會執行的程式碼區塊。beforeEach()
:在每個 Test Case 開始前執行的程式碼區塊。afterEach()
:在每個 Test Case 結束後執行的程式碼區塊。
語法範例
describe('hooks', function() {
// 測試區塊
before(function() {
// 在所有測試開始前會執行的程式碼區塊
});
after(function() {
// 在所有測試結束後會執行的程式碼區塊
});
beforeEach(function() {
// 在每個 Test Case 開始前執行的程式碼區塊
});
afterEach(function() {
// 在每個 Test Case 結束後執行的程式碼區塊
});
// 撰寫個別 Test Case
it('should ...', function() {
// 執行 Test Case
});
});
TDD vs BDD
比較 TDD 與 BDD 的差異。
TDD | BDD | |
---|---|---|
全名 | 測試驅動開發 Test-Driven Development |
行為驅動開發 Behavior Driven Development |
定義 | 在開發前先撰寫測試程式,以確保程式碼品質與符合驗收規格。 | TDD 的進化版。 除了實作前先寫測試外,還要寫一份「可以執行的規格」。 |
特性 | 從測試去思考程式如何實作。 強調小步前進、快速且持續回饋、擁抱變化、重視溝通、滿足需求。 |
從用戶的需求出發,強調系統行為。 使用自然語言描述測試案例 ,以減少使用者和工程師的溝通成本。 測試後的輸出結果可以直接做為文件閱讀。 |
Chai
Chai 提供 BDD 語法測試用的斷言庫(Assertion Library)。斷言庫是一種判斷工具,驗證執行結果是否符合預期,若實際結果和預測不同,就是測到 bug 了。以下分 Assert 和 Expect / Should 說明。
Assert
assert(expression, message)
:測試這個項目的 expression 'foo' === 'bar'
是否為真,若為假則顯示錯誤訊息 message。
var assert = chai.assert;
describe('AssertTest', function() {
var foo = 'Hello';
var bar = 'World';
it('should be equal', function() {
assert('foo' === 'bar', 'foo is not bar');
});
});
Expect / Should
預期 3 等於(===)2。這是使用可串連的 getters 來完成斷言。這些可串聯的 getters 有 to、is、have 等。它很像英文,用很口語的方式做判斷。
var expect = chai.expect;
describe('ExpectTest', function() {
it('should be equal', function() {
expect(3).to.equal(2);
});
});
測試 add(兩數相加)和 sub(兩數相減)功能。
// calculateModule
function _add(x, y) {
return x + y;
}
function _sub(x, y) {
return x - y;
}
module.exports = {
add: _add,
sub: _sub,
};
const expect = require('chai').expect;
const testModule = require('../modules/calculateModule');
describe('測試 add', () => {
it('1 + 2 = 3', () => {
expect(testModule.add(1, 2)).to.equal(3);
});
it('3 + 4 = 7', () => {
expect(testModule.add(3, 4)).to.equal(7);
});
});
describe('測試 sub', () => {
it('1 - 2 = -1', () => {
expect(testModule.sub(1, 2)).to.equal(-1);
});
it('11 - 4 = 7', () => {
expect(testModule.sub(11, 4)).to.equal(7);
});
});
比較 Assert、Expect、Should 的差異
三者基本上都可完成相同工作,除了
- Should 會修改 Object.prototype
- Should 在瀏覽器環境下,對 IE 有相容問題
- Should 無法客製化錯誤訊息
// Assert 和 Expect 的客製化錯誤訊息範例
assert.isTrue(foo, 'foo should be true');
expect(foo, 'foo should be true').to.be.true;
Sinon
用來產生 Test Double(測試替身),可當成假資料來看,分為 Spy、Stub 和 Mock。
Spy
對 function call 蒐集資訊,便於對測試結果做驗證。sinon.spy()
會回傳一個 Spy 物件,這個 Spy 物件像蜜糖般包裹於原 function 外,讓我們可以像 function 一樣呼叫。而這個 Spy 物件的 property 會協助蒐集由 function call 得到的資訊,例如:取得第一次呼叫所輸入的參數、該 function 被呼叫的次數。Spy 是三者最簡單的部份,並且 Stub 和 Mock 是建構於 Spy 之上的。
const expect = require('chai').expect;
const sinon = require('sinon');
const testModule = require('../modules/comparisionModule');
describe('測試 comparePelple', function() {
it('should call the callback function', function() {
var nameList = ['Nina', 'Ricky'];
var callback = sinon.spy();
testModule.comparePelple(nameList[0], nameList[0], callback);
console.log(callback.callCount); // 該 function 被呼叫的次數
});
});
Stub
取代 function。與 Spy 不同的是,Spy 依然會執行真的 function,但 Stub 並不會。適用於 Ajax 和 Timer。
範例
const expect = require('chai').expect;
const sinon = require('sinon');
const uuid = require('node-uuid');
describe('stub example', () => {
it('check length of uuid ', () => {
var stub = sinon.stub(uuid, 'v4');
var mockId = stub.v4();
expect(mockId.length).to.equal(36);
uuid.v4.restore();
});
});
範例:Ajax Request
function saveUser(user, callback) {
$.ajax(
'/users',
{
first: user.firstname,
last: user.lastname,
},
callback,
);
}
describe('Stub: ajax', () => {
it('should call callback after saving', () => {
var ajax = sinon.stub($, 'ajax'); // 取代 ajax function,並不會真的呼叫
ajax.yields('Hello', 'World'); // 準備把傳入 callback 的參數 ['Hello', 'World'] 丟進去
var callback = sinon.spy();
saveUser({ firstname: 'Han', lastname: 'Solo' }, callback);
ajax.restore(); // 清除 test double
expect(callback.callCount).to.equal(777); // 該 function 被呼叫的次數
});
});
Mock
取代整個物件,包含完整實作細節。
var opts = {
call: function(msg) {
console.log(msg);
},
};
describe('Mock', () => {
it('should pass Hello World to run call()', function() {
var mock = sinon.mock(opts);
mock
.expects('call')
.once()
.withExactArgs('Hello World');
opts.call('Hello World');
mock.restore();
mock.verify();
});
});
使用時機
總結一下三者使用的時機…
- Spy:用於監看或驗證。
- Stub:使用一些假資料,測試各種狀況下功能的運作狀況。
- Mock:除了上述 Spy 和 Stub 的情況外,還需隔絕測試過程於完全獨立的環境。
Chai Assertion vs Sinon Assertion
推薦使用 Sinon 的 Assertion,因為不需刻意客製化報錯,即可反應問題所在。
Chai Assertion 報錯範例。
assert('foo' === 'bar');
報錯如下,但難以了解問題所在。
AssertionError: Unspecified AssertionError
加入客製化報錯,清楚明瞭。
assert('foo' === 'bar', 'foo is not bar');
AssertionError: foo is not bar
Sinon Assertion 報錯範例。
var expected = { x: 999 },
actual = { x: 1, y: 2 };
sinon.assert.match(actual, expected);
報錯如下,無需刻意客製化報錯,即可反應問題所在。
AssertError: expected value to match
expected = { x: 999 }
actual = { x: 1, y: 2 }
QnA
寫測試是否會增加額外工時?
工時是一定會增加的,個人經驗是增加一倍。
除了程式碼的品質保證外,還有什麼好處?
記錄規格。測試案例如同告知開發者規格的細節和範例,再也不怕同事離職,無人可問。
附上露天讀書會投影片-單元測試:Mocha、Chai 和 Sinon。