通過 Sinon.js 在 JavaScript 中使用間諜進行測試
簡介
在軟件測試中,“間諜”記錄了一個函數在測試時是如何使用的。這包括它被調用了多少次,是否使用正確的參數調用它,以及返回了什麼。
雖然測試主要用於驗證函數的輸出,但有時我們需要驗證函數與代碼其他部分的交互方式。
在本文中,我們將更深入地了解什麼是間諜以及何時應該使用它們。然後,我們將在 JavaScript 單元測試中使用 Sinon.js 時監視 HTTP 請求。
本文是關於使用 Sinon.js 進行測試技術的系列文章的第二篇。我們建議您也閱讀我們之前的文章:
- 在帶有 Sinon.js 的 JavaScript 中使用存根進行測試
- 在帶有 Sinon.js 的 JavaScript 中使用間諜進行測試(你在這裡 )
- 在帶有 Sinon.js 的 JavaScript 中使用 Mocks 進行測試
什麼是間諜?
間諜是測試中跟踪對方法的調用的對象。通過跟踪它的調用,我們可以驗證它的使用方式是否符合我們函數的預期使用方式。
正如其名,間諜為我們提供了有關如何使用函數的詳細信息。它被調用了多少次?函數傳入了什麼參數?
讓我們考慮一個檢查用戶是否存在的函數,如果不存在則在我們的數據庫中創建一個。我們可能會存根數據庫響應並在我們的測試中獲取正確的用戶數據。但是,在我們沒有預先存在的用戶數據的情況下,我們如何知道該函數實際上是在創建用戶呢?通過spy,我們會觀察create-user函數被調用了多少次並確定。
現在我們知道了間諜是什麼,讓我們考慮一下我們應該使用它們的情況。
為什麼要使用間諜?
間諜擅長洞察我們正在測試的函數的行為。雖然驗證測試的輸入和輸出至關重要,但在許多情況下檢查函數的行為方式也至關重要:
當您的函數具有未反映在其結果中的副作用時,您應該監視它使用的方法。
一個示例是在多次調用各種外部 API 後將 JSON 返回給用戶的函數。最終的 JSON 有效負載不會告訴用戶該函數如何檢索其所有數據。監視它調用外部 API 的次數以及在這些調用中使用了哪些輸入的間諜會告訴我們如何調用。
讓我們看看如何使用 Sinon.js 在我們的代碼中創建間諜。
使用 Sinon.Js 創建 Spy
有多種方法可以使用 Sinon.js 創建間諜,每種方法都有其優點和缺點。本教程將重點介紹以下兩種方法,它們一次針對單個函數進行間諜活動:
- 跟踪參數、值和對方法的調用的匿名函數。
- 現有函數的包裝器。
首先,讓我們設置我們的項目,以便我們可以運行我們的測試文件並使用 Sinon.js。
設置
讓我們首先創建一個文件夾來存儲我們的 JavaScript 代碼。創建一個新文件夾並移入其中:
$ mkdir SpyTests
$ cd SpyTests
初始化 NPM,以便您可以跟踪您安裝的包:
$ npm init -y
現在讓我們安裝我們的測試依賴項。我們安裝 Mocha 和 Chai 以及 Sinon.js 來運行我們的測試:
$ npm i mocha chai sinon --save-dev
我們的設置完成了!讓我們從使用間諜作為匿名函數開始。
具有匿名函數的間諜
作為匿名函數,Sinon.js 間諜通常在我們想要測試接受其他函數(即回調作為參數)的高階函數的情況下很有用。讓我們看一個重新實現 Array.prototype.map()
的基本示例 帶有回調:
創建兩個文件,即 mapOperations.js
和 mapOperations.test.js
spyTests
裡面 目錄如下:
$ touch mapOperations.js mapOperations.test.js
在mapOperations.js
中輸入以下代碼 文件:
const map = (array, operation) => {
let arrayOfMappedItems = [];
for (let item of array) {
arrayOfMappedItems.push(operation(item));
}
return arrayOfMappedItems;
};
console.log(map([{ name: 'john', role: 'author'}, { name: 'jane', role: 'owner'}], user => user.name));
module.exports = { map };
免費電子書:Git Essentials
查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!
在上面的代碼中,map()
接受一個數組作為它的第一個參數和一個回調函數,operation()
,將數組項轉換為其第二個參數。
map()
內部 函數,我們遍歷數組並對每個數組項應用操作,然後將結果推送到 arrayOfMappedItems
數組。
當您在控制台上運行此示例時,您應該會得到以下結果:
$ node mapOperations.js
[ 'john', 'jane' ]
測試是否operation()
函數被我們的 map()
調用 函數,我們可以創建一個匿名間諜並將其傳遞給 map()
功能如下:
const { map } = require('./mapOperations');
const sinon = require('sinon');
const expect = require('chai').expect;
describe('test map', () => {
const operation = sinon.spy();
it('calls operation', () => {
map([{ name: 'foo', role: 'author'}, { name: 'bar', role: 'owner'}], operation);
expect(operation.called);
});
});
雖然我們的回調實際上並沒有轉換數組,但我們的間諜可以驗證我們正在測試的函數是否實際使用了它。這在 expect(operation.called);
時得到確認 沒有通過測試。
讓我們看看我們的測試是否通過!運行測試,應該會得到如下輸出:
$ mocha mapOperations.test.js
test map
✓ calls operation
1 passing (4ms)
✨ Done in 0.58s.
有用!我們現在確定我們的函數將使用我們在其參數中放入的任何回調。現在讓我們看看如何使用 spy 包裝函數或方法。
間諜作為函數或方法包裝器
在上一篇文章中,我們看到瞭如何在單元測試中存根 HTTP 請求。我們將使用相同的代碼來展示如何使用 Sinon.js 來監視 HTTP 請求。
在一個名為 index.js
的新文件中 ,添加如下代碼:
const request = require('request');
module.exports = {
getAlbumById: async function(id) {
const requestUrl = `https://jsonplaceholder.typicode.com/albums/${id}/photos?_limit=3`;
return new Promise((resolve, reject) => {
request.get(requestUrl, (err, res, body) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(body));
});
});
}
};
回顧一下,getAlbumById()
方法調用一個 JSON API,它從我們將其 ID 作為參數傳遞的相冊中獲取照片列表。以前,我們將 request.get()
存根 方法返回一個固定的照片列表。
這一次,我們將監視 request.get()
方法,以便我們可以驗證我們的函數是否向 API 發出了 HTTP 請求。我們還將檢查它是否發出過一次請求,這很好,因為我們不希望出現垃圾郵件 API 端點的錯誤。
在一個名為 index.test.js
的新測試文件中 ,逐行編寫如下JavaScript代碼:
const expect = require('chai').expect;
const request = require('request');
const sinon = require('sinon');
const index = require('./index');
describe('test getPhotosByAlbumId', () => {
let requestSpy;
before(() => {
requestSpy = sinon.spy(request, 'get');
});
after(() => {
request.get.restore();
});
it('should getPhotosByAlbumId', (done) => {
index.getAlbumById(2).then((photos) => {
expect(requestSpy.calledOnce);
expect(requestSpy.args[0][0]).to.equal("https://jsonplaceholder.typicode.com/albums/2/photos?_limit=3");
photos.forEach(photo => {
expect(photo).to.have.property('id');
expect(photo).to.have.property('title');
expect(photo).to.have.property('url');
});
done();
});
});
});
在上面的測試中,我們包裝了 request.get()
before()
中設置期間使用間諜的方法 功能。當我們在 after()
中拆除測試時,我們恢復該功能 功能。
在測試用例中,我們斷言 requestSpy
, 跟踪 request.get()
的對象 的用法,只記錄一次通話。然後我們更深入地確認 request.get()
的第一個參數 call 是 JSON API 的 URL。然後我們做出斷言以確保返回的照片具有預期的屬性。
運行測試時,應該會得到以下輸出:
$ mocha index.test.js
test getPhotosByAlbumId
✓ should getPhotosByAlbumId (570ms)
1 passing (587ms)
✨ Done in 2.53s.
請注意,此測試向 API 發出了實際的網絡請求。間諜包圍了 函數,它確實不 替換它的功能!
此外,Sinon.js 測試存根已經是間諜!如果您曾經創建過測試存根,您將能夠看到它被調用了多少次以及傳遞給函數的參數。
結論
測試中的間諜為我們提供了一種跟踪對方法的調用的方法,以便我們可以驗證它是否按預期工作。我們使用 spies 來檢查一個方法是否被調用,被調用了多少次,被調用的參數是什麼,調用時返回的值。
在本文中,我們介紹了間諜的概念,並了解瞭如何使用 Sinon.js 創建間諜。我們還研究瞭如何將間諜創建為匿名函數,以及如何使用它們來包裝方法。對於更高級的用例,Sinon.js 提供了我們可以利用的豐富的間諜 API。更多詳細信息,可在此處訪問文檔。