Author:
bugall
Wechat:bugallF
Email:769088641@qq.com
專案地址: https://github.com/bugall/nod…
為什麼需要Sinon
當我們在開發前端專案的時候, 很多時候需要根據後端返回的資料來渲染頁面, 我們通常使用AJAX傳送請求給服務端。當我們開發後端邏輯的時候有時候需要連線資料庫,根據從資料庫中得到的資料來執行後續的邏輯程式碼, 或者其他的依賴, 甚至會更加複雜棘手。這些開發都存在一個共同的侷限性, 就是會去依賴別的服務, 需要別的系統的支援。 例如, 如果我們使用Ajax請求網路, 您需要有一個伺服器來響應對應的請求。對於資料庫, 您需要有一個為測試設定的測試資料庫。
所有這些都意味著編寫和執行測試更加困難, 因為您需要做額外的工作來準備和設定一個測試成功的環境。
值得慶幸的是, 我們可以用sinon.js避免所有麻煩。我們可以利用它的特性將上面的例子簡化為幾行程式碼。
然而, 第一次接觸spies
, stub
, mock
可能棘手。它可能很難選擇什麼時候用什麼功能。它們也有一些問題,所以你需要知道你應該用什麼功能解決什麼樣的問題。
在這篇文章中將向你展示spies
, stub
, mock
何時以及如何使用它們,並給你一套最佳實踐,幫助您避免常見的陷阱.
什麼是Sinon
Sinon
具有獨立的spies, stub, mock功能,Sinon
並不是獨立的測試框架,它只是在測試中提供了上述的三種功能, 例如我們常用的測試框架Mocha
,Sinon
並不能完全替代Mocha
的功能。
Sinon
通過所謂的測試替代(test-double
)輕鬆消除測試的複雜度,
測試替代,顧名思義,測試中用到的是真實程式碼邏輯的替代品。回過頭來看Ajax示例,我們不需要設定伺服器,而是用Ajax的替代程式碼,我們把Ajax的邏輯替換成不需要通過請求伺服器就返回預先設定好的資料,這聽起來有不可思議,但是基本概念很簡單。因為JavaScript是動態的,所以我們可以在呼叫某個方法的時候使用任何函式來替換它。在Sinon
中們可以用一個測試邏輯取代任何JavaScript函式,然後讓測試複雜的事情變的簡單化。
spies的概念
顧名思義,spies
我們乾脆就把它稱作間諜函式好了,間諜函式是Sinon
最簡單的部分,其它的功能都是建立在spies
之上的,spies
的主要用途是收集有關函式呼叫的資訊。您還可以使用它們來幫助驗證事物,例如是否呼叫了函式等。就像電影《竊聽風雲》中一樣,監聽房間內都有那些人進出,做了什麼事,而且這個監聽過程是不會房間內的人感知的。同樣spies
的實現監聽的基礎上是不會影響函式本身的正常呼叫(被監聽的函式的上下文關係不會被影響)。當然我們實現是需要在房間裡偷偷的安裝竊聽器
的, 那麼spies
的竊聽器是如何實現的呢?後文我們有介紹
stub的概念
他們擁有spies
的所有功能,不是監視某個函式的呼叫情況,而是完全取代了這個函式。換句話說,當使用spies
時,原始函式仍然執行,但是當使用stub
時,函式將不具有原始的功能,而是替換後的函式。
mock的概念
mock
與stub
的功能一樣都是用來替換指定的函式,如果你想替換掉一個物件中的多個方法,這時mock
就可以發揮作用了,但是如果僅僅是替換物件中的一個函式,那麼stub
更加簡單易用,當我們使用mock
的時候應該十分小心,因為大量的替換原有程式碼邏輯,會導致test變的脆弱
,
Sinon的使用場景
spies
正如名字所暗示的,spies
被用來獲取關於函式呼叫的資訊。例如,一個spies
可以告訴我們呼叫一個函式的次數、每次呼叫的引數、返回的值、丟擲的錯誤等。因此,當測試的目的是驗證發生的事情時,間諜是一個很好的選擇。結合Sinon
的說法,我們可以通過一個簡單的spies
檢查不同的結果。
間諜最常見的場景包括:
-
檢查函式被呼叫了多少次
it(`should call save once`, function() {
var save = sinon.spy(Database, `save`);
setupNewUser({ name: `test` }, function() { });
save.restore();
sinon.assert.calledOnce(save);
});
-
檢查傳遞給函式的引數
it(`should pass object with correct values to save`, function() {
var save = sinon.spy(Database, `save`);
var info = { name: `test` };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
setupNewUser(info, function() { });
save.restore();
sinon.assert.calledWith(save, expectedUser);
});
stub
存根就像間諜,除了它們替換目標功能。它們還可以包含自定義行為,例如返回值,或丟擲異常。他們甚至可以自動呼叫作為引數提供的任何回撥函式。
存根有幾個常用的用途:
-
您可以使用它們來代替有問題的程式碼段
-
您可以使用它們來觸發不會觸發的程式碼路徑,例如錯誤處理
-
您可以使用它們來幫助測試非同步程式碼更容易
-
存根可用於替代有問題的程式碼,即使寫入測試困難的程式碼。這通常是外部網路連線,資料庫或其他非JavaScript引起的。這些問題是它們經常需要手動設定。例如,在執行測試之前,我們需要填寫一個帶有測試資料的資料庫,這使得執行和寫入更復雜。
it(`should pass object with correct values to save`, function() {
var save = sinon.stub(Database, `save`);
var info = { name: `test` };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
setupNewUser(info, function() { });
save.restore();
sinon.assert.calledWith(save, expectedUser);
});
通過用stub替換與資料庫相關的功能,我們不再需要實際的資料庫進行測試。 幾乎任何情況下,類似的方法都可以用於其他難以測試的程式碼。
存根也可用於觸發不同的程式碼路徑。 如果我們測試的程式碼呼叫另一個函式,我們有時需要測試它在異常條件下的行為, 我們可以使用存根從程式碼中觸發錯誤:
it(`should pass the error into the callback if save fails`, function() {
var expectedError = new Error(`oops`);
var save = sinon.stub(Database, `save`);
save.throws(expectedError);
var callback = sinon.spy();
setupNewUser({ name: `foo` }, callback);
save.restore();
sinon.assert.calledWith(callback, expectedError);
});
mock
主要用於當你存根的時候想驗證多個具體的行為時
例如,以下是我們如何使用mock
驗證更具體的資料庫儲存方案:
it(`should pass object with correct values to save only once`, function() {
var info = { name: `test` };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
var database = sinon.mock(Database);
database.expects(`save`).once().withArgs(expectedUser);
setupNewUser(info, function() { });
database.verify();
database.restore();
});
Sinon的實現原理
spies
const sinon = {
spyObjs: {},
spy: function(obj, method) {
const self = this;
this.spyObjs[`spy#:` + (Object.keys(self.spyObjs).length + 1)] = {}
this.proxy(obj, method);
}, proxy: function(obj, method) {
const descriptor = Object.getOwnPropertyDescriptor(obj, method);
const delegateFlag = `spy#:` + Object.keys(sinon.spyObjs).length;
this.spyObjs[delegateFlag] = {
delegateValue: descriptor.value,
delegateObject: obj
}
Object.defineProperty(obj, method, Object.getOwnPropertyDescriptor(this, `invoke`))
}, invoke: function(name) {
console.log(`引數%s, 被呼叫了`, name)
const delegateFlag = `spy#:` + Object.keys(sinon.spyObjs).length;
sinon.spyObjs[delegateFlag].delegateValue.apply(sinon.spyObjs[delegateFlag].delegateObject)
}
}
var testFlag = {
sayHello: function(name) {
console.log(`Hello:%s`, name)
}, whoAmI: function() {
this.sayHello(`bugall`)
console.log(`Who am i`)
}
}
sinon.spy(testFlag, `whoAmI`);
testFlag.whoAmI(`bugall`)