Sinon 入門,看這篇文章就夠了

李騰飛發表於2019-02-16

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的概念

mockstub的功能一樣都是用來替換指定的函式,如果你想替換掉一個物件中的多個方法,這時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`)

相關文章