[Jest]單元測試初學者指南 - 第三部分 - 模擬Http請求和訪問檔案
原文: Unit Testing Beginners Guide - Part 3 - Mock Http And Files access
作者:jstweetster
在本文結束時,您將能夠使用Jest正確的測試包含http請求,檔案訪問方法,資料庫呼叫或者任何其他型別的輔助作用的生產程式碼。此外,您將學習如何處理其他類似的問題,而且您需要避免實際呼叫的方法活模組(例如資料庫呼叫)。
譯者注:接下來的測試涉及到Jest需要使用Babel的情況,所以需要安裝一些依賴,可以檢視官方文件說明 https://jestjs.io/docs/zh-Hans/getting-started ,也可以直接檢視上面給出的示例程式碼。
1)安裝依賴
yarn add --dev babel-jest babel-core regenerator-runtime babel-preset-env # 也可以使用 npm 安裝
2)在專案根目錄新建 .babelrc
檔案,新增內容如下:
{ "presets": ["env"] }
模擬HTTP請求
讓我們假設我們有一段程式碼使用 XMLHttpRequest
方法執行網路請求,如下:
const API_ROOT = 'http://jsonplaceholder.typicode.com'; class API { getPosts() { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', `${API_ROOT}/posts`); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { const resp = JSON.parse(xhr.responseText); if (resp.error) { reject(resp.error); } else { resolve(resp); } } } xhr.send(); }) } } export default new API();
我們怎樣測試這個方法?
首先,讓我們定義這個方法是幹什麼的。
getPosts()
使用API呼叫來檢索部落格中的帖子列表,並且返回一個解析帖子列表的 Promise
我們想要避免的是在我們的測試中實際進行API呼叫。
為什麼?
- 因為呼叫的資源可能在執行測試的環境中不可用
- 因為我們有第三方依賴(API端點),這是的測試容易失敗。
- 因為測試將會執行的很慢
- 因為端點呼叫可能具有需要清理的意外後果(例如寫入DB)
- 因為你不再進行單元測試了。相反,我們將進行功能測試領域了
- 最後,因為被呼叫的資源可能在時間和複雜性方面執行昂貴的操作
正如你所看到的,我們有很多理由希望避免直接在具有實際網路請求單元測試中工作。
模擬 XMLHttpRequest
解決方法是回溯到使用 XMLHttpRequest
物件的正確模擬,攔截對它的呼叫並偽造行為。
如果你不記得究竟模擬什麼和怎樣使用,你可以檢視 “使用Jest中的spies和fake timers” 和Jest官方文件對 mocks 的說明。
讓我們嘗試建立第一個測試(假設我們有一個 api.spec.js
檔案):
import API from '../src/api.js'; const mockXHR = { open: jest.fn(), send: jest.fn(), readyState: 4, responseText: JSON.stringify( [ { title: 'test post' }, { tile: 'second test post' } ] ) }; const oldXMLHttpRequest = window.XMLHttpRequest; window.XMLHttpRequest = jest.fn(() => mockXHR); describe('API integration test suite', function () { test('Should retrieve the list of posts from the server when calling getPosts method', function (done) { const reqPromise = API.getPosts(); mockXHR.onreadystatechange(); reqPromise.then((posts) => { expect(posts.length).toBe(2); expect(posts[0].title).toBe('test post'); expect(posts[1].title).toBe('second test post'); done(); }); }); });
讓我們一步一步的來了解發生了什麼
const mockXHR = { open: jest.fn(), send: jest.fn(), readyState: 4, responseText: JSON.stringify( [ { title: 'test post' }, { title: 'second test post' } ] ) };
我們開始建立了一個假的XHR物件,實際的 open
和 send
方法是不做任何事情的函式。我們還設定 readyState
的值為4(通常用於檢測請求是否已完成)和 responseText
的值為適合我們要測試的內容。
我們可以通過為 responseText
提供正確的文字值來模擬我們想要的任何API響應。
const oldXMLHttpRequest = window.XMLHttpRequest; window.XMLHttpRequest = jest.fn(() => mockXHR);
接下來,我們將備份內建的 XMLHttpRequest
物件,將其替換為返回我們的模擬物件的函式。備份真正的 XMLHttpRequest
物件是一個好主意,因為在測試結束時我們應該清理環境,讓環境處於使用它之前的初始狀態。
因此,每當呼叫 new XMLHttpRequest
時,將會返回 mockXHR
物件。
而且,最後我們使用這個設定,對 getPosts
函式進行單元測試更加容易啦。
const reqPromise = API.getPosts(); mockXHR.onreadystatechange();
呼叫這個 API ,然後通過呼叫 onreadystatechange
函式來模擬響應到達。當 onreadystatechange
被呼叫後,我們實際上正在呼叫在 api.js
中設定的狀態更改的回撥函式:
xhr.onreadystatechange = function () { if (xhr.readyState === 4) { const resp = JSON.parse(xhr.responseText); if (resp.error) { reject(resp.error); } else { resolve(resp); } } }
因為模擬的 XHR 物件的 readyState
的值設定為 4
,Promise 物件將會立即被解析(resolved)或拒絕(rejected),具體取決於響應。
根據斷言,我們驗證從 getPosts()
函式返回的 Promise
物件是否具有實際的 JSON 響應值作為被解析的值。
我們通過檢查文章數量是否正確以及每篇文章應該是什麼值來驗證測試的正確性。
expect(posts.length).toBe(2); expect(posts[0].title).toBe('test post'); expect(posts[1].title).toBe('second test post');
深入理解
我們可以將該方法進行一些改進,使其更靈活和易於使用。
首先,讓我們建立一個可以模擬 XHR 物件的工廠函式,它能夠讓我們更加容易的建立新的模擬 XHR,並且,可選擇的指定響應的物件。
const createMockXHR = (responseJSON) => { const mockXHR = { open: jest.fn(), send: jest.fn(), readyState: 4, responseText: JSON.stringify( responseJSON || {} ) }; return mockXHR; }
接下來,讓我們為每一個單元測試建立一個新的 XHR 物件。在單元測試時,我們最不希望的是在 單元測試中具有共享狀態 ,這會導致不可預測且難以除錯的測試。實際的測試單元如下:
describe('API integration test suite', function() { const oldXMLHttpRequest = window.XMLHttpRequest; let mockXHR = null; beforeEach(() => { mockXHR = createMockXHR(); window.XMLHttpRequest = jest.fn(() => mockXHR); }); afterEach(() => { window.XMLHttpRequest = oldXMLHttpRequest; }); test('Should retrieve the list of posts from the server when calling getPosts method', function(done) { const reqPromise = API.getPosts(); mockXHR.responseText = JSON.stringify([ { title: 'test post' }, { title: 'second test post' } ]); mockXHR.onreadystatechange(); reqPromise.then((posts) => { expect(posts.length).toBe(2); expect(posts[0].title).toBe('test post'); expect(posts[1].title).toBe('second test post'); done(); }); }); });
此外,在每次測試之後,我們通過在 beforeEach
中模擬 XMLHttpRequest
並在 afterEach
中恢復原本的 XMLHttpRequest
物件來清理環境。
beforeEach(() => { mockXHR = createMockXHR(); window.XMLHttpRequest = jest.fn(() => mockXHR); }); afterEach(() => { window.XMLHttpRequest = oldXMLHttpRequest; });
我們獲得的另一個好處是我們可以非常輕鬆的測試不同的場景。假設我們想要新增另一個測試,模擬 API 返回錯誤:
test('Should return a failed promise with the error message when the API returns an error', function(done) { const reqPromise = API.getPosts(); mockXHR.responseText = JSON.stringify({ error: 'Failed to GET posts' }); mockXHR.onreadystatechange(); reqPromise.catch((err) => { expect(err).toBe('Failed to GET posts'); done(); }); });
你是否注意到模擬API不同的響應變得更加容易啦?
如何模擬檔案系統/資料庫呼叫和其他副作用(side effects)
我們可以非常輕鬆地擴充套件我們應用於HTTP請求的技術,以涵蓋其他型別的副作用。
假設我們有一個 FileSystem
元件,它有一個讀取檔案並將其解析為 JSON 的方法:
import fs from 'fs'; export default class FileSystem { parseJSONFile(file) { const content = String(fs.readFileSync(file)); return JSON.parse(content); } }
我們想要測試 parseJSONFile()
方法,但是我們也想要避免從磁碟中建立檔案和讀取它的內容。
我們的測試單元如下:
jest.mock('fs', () => ({ readFileSync: jest.fn() })); import FileSystem from './FileSystem.js'; import fs from 'fs'; describe('FileSystem test suite', function() { test('Should return the parsed JSON from a file specified as param', function(done) { const fileReader = new FileSystem(); fs.readFileSync.mockReturnValue('{ "test": 1 }'); const result = fileReader.parseJSONFile('test.json'); expect(result).toEqual({ "test": 1 }); done(); }); });
讓我們一步一步的來看:
jest.mock('fs', () => ({ readFileSync: jest.fn() })})
jest.mock
允許我們模擬我們可能擁有的任何模組,包括在 NodeJS 中構建的並有工廠函式的模組作為第二個引數 (arg),返回模擬的返回值。
在我們的示例中,每當我們的程式碼裡有 const fs = require('fs');
或者 import fs from 'fs'
,匯入的值實際上是我們從工廠函式返回的物件:
{ readFileSync: jest.fn() }
在使用 fs.readFileSync
之前呼叫 jest.mock
很重要。
接下來,我們例項化我們要測試的元件 const fileReader = new FileSystem();
,並指示 readFileSync
spy 返回某個預先製作的字串:
fs.readFileSync.mockRetrunValue('{ "test": 1 }');
如果你想要了解更多的操作資訊,請在 Jest 文件中檢視 mockReturnValue 。
最後,我們驗證 parseJSONFile
的結果是解析的 JSON 值:
expect(result).toEqual({ "test": 1 });
以上是對模擬http呼叫和檔案系統呼叫的介紹。

掃碼關注w3ctech微信公眾號