Author:
bugall
Wechat:bugallF
Email:769088641@qq.com
項目地址: https://github.com/bugall/nod...前端
當咱們在開發前端項目的時候, 不少時候須要根據後端返回的數據來渲染頁面, 咱們一般使用AJAX發送請求給服務端。當咱們開發後端邏輯的時候有時候須要鏈接數據庫,根據從數據庫中獲得的數據來執行後續的邏輯代碼, 或者其餘的依賴, 甚至會更加複雜棘手。這些開發都存在一個共同的侷限性, 就是會去依賴別的服務, 須要別的系統的支持。 例如, 若是咱們使用Ajax請求網絡, 您須要有一個服務器來響應對應的請求。對於數據庫, 您須要有一個爲測試設置的測試數據庫。node
全部這些都意味着編寫和運行測試更加困難, 由於您須要作額外的工做來準備和設置一個測試成功的環境。
值得慶幸的是, 咱們能夠用sinon.js避免全部麻煩。咱們能夠利用它的特性將上面的例子簡化爲幾行代碼。git
然而, 第一次接觸spies
, stub
, mock
可能棘手。它可能很難選擇何時用什麼功能。它們也有一些問題,因此你須要知道你應該用什麼功能解決什麼樣的問題。
在這篇文章中將向你展現spies
, stub
, mock
什麼時候以及如何使用它們,並給你一套最佳實踐,幫助您避免常見的陷阱.github
Sinon
具備獨立的spies, stub, mock功能,Sinon
並非獨立的測試框架,它只是在測試中提供了上述的三種功能, 例如咱們經常使用的測試框架Mocha
,Sinon
並不能徹底替代Mocha
的功能。數據庫
Sinon
經過所謂的測試替代(test-double
)輕鬆消除測試的複雜度,
測試替代,顧名思義,測試中用到的是真實代碼邏輯的替代品。回過頭來看Ajax示例,咱們不須要設置服務器,而是用Ajax的替代代碼,咱們把Ajax的邏輯替換成不須要經過請求服務器就返回預先設置好的數據,這聽起來有難以想象,可是基本概念很簡單。由於JavaScript是動態的,因此咱們能夠在調用某個方法的時候使用任何函數來替換它。在Sinon
中們能夠用一個測試邏輯取代任何JavaScript函數,而後讓測試複雜的事情變的簡單化。後端
顧名思義,spies
咱們乾脆就把它稱做間諜函數好了,間諜函數是Sinon
最簡單的部分,其它的功能都是創建在spies
之上的,spies
的主要用途是收集有關函數調用的信息。您還可使用它們來幫助驗證事物,例如是否調用了函數等。就像電影《竊聽風雲》中同樣,監聽房間內都有那些人進出,作了什麼事,並且這個監聽過程是不會房間內的人感知的。一樣spies
的實現監聽的基礎上是不會影響函數自己的正常調用(被監聽的函數的上下文關係不會被影響)。固然咱們實現是須要在房間裏偷偷的安裝竊聽器
的, 那麼spies
的竊聽器是如何實現的呢?後文咱們有介紹服務器
他們擁有spies
的全部功能,不是監視某個函數的調用狀況,而是徹底取代了這個函數。換句話說,當使用spies
時,原始函數仍然運行,可是當使用stub
時,函數將不具備原始的功能,而是替換後的函數。網絡
mock
與stub
的功能同樣都是用來替換指定的函數,若是你想替換掉一個對象中的多個方法,這時mock
就能夠發揮做用了,可是若是僅僅是替換對象中的一個函數,那麼stub
更加簡單易用,當咱們使用mock
的時候應該十分當心,由於大量的替換原有代碼邏輯,會致使test變的脆弱
,app
正如名字所暗示的,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); });
存根就像間諜,除了它們替換目標功能。它們還能夠包含自定義行爲,例如返回值,或拋出異常。他們甚至能夠自動調用做爲參數提供的任何回調函數。
存根有幾個經常使用的用途:
您可使用它們來代替有問題的代碼段
您可使用它們來觸發不會觸發的代碼路徑,例如錯誤處理
您可使用它們來幫助測試異步代碼更容易
存根可用於替代有問題的代碼,即便寫入測試困難的代碼。這一般是外部網絡鏈接,數據庫或其餘非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
驗證更具體的數據庫保存方案:
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(); });
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')