在本篇教程中,我們會介紹 Jest
中的三個與 Mock
函數相關的API,分別是jest.fn()
、jest.spyOn()
、jest.mock()
。使用它們創建Mock函數能夠幫助我們更好的測試項目中一些邏輯較複雜的代碼,例如測試函數的嵌套調用,回調函數的調用等。
如果你還不知道
Jest
的基本使用方法,請先閱讀: 《使用Jest測試JavaScript (入門篇)》
爲什麼要使用Mock函數?
在項目中,一個模塊的方法內常常會去調用另外一個模塊的方法。在單元測試中,我們可能並不需要關心內部調用的方法的執行過程和結果,只想知道它是否被正確調用即可,甚至會指定該函數的返回值。此時,使用Mock函數是十分有必要。
Mock函數提供的以下三種特性,在我們寫測試代碼時十分有用:
- 捕獲函數調用情況
- 設置函數返回值
- 改變函數的內部實現
我們接着使用上篇文章中的目錄結構,在
test/functions.test.js
文件中編寫測試代碼,src/
目錄下寫被測試代碼。
1. jest.fn()
jest.fn()
是創建Mock函數最簡單的方式,如果沒有定義函數內部的實現,jest.fn()
會返回undefined
作爲返回值。
// functions.test.js
test('測試jest.fn()調用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 斷言mockFn的執行後返回undefined
expect(result).toBeUndefined();
// 斷言mockFn被調用
expect(mockFn).toBeCalled();
// 斷言mockFn被調用了一次
expect(mockFn).toBeCalledTimes(1);
// 斷言mockFn傳入的參數爲1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
jest.fn()
所創建的Mock函數還可以設置返回值,定義內部實現或返回Promise
對象。
// functions.test.js
test('測試jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 斷言mockFn執行後返回值爲default
expect(mockFn()).toBe('default');
})
test('測試jest.fn()內部實現', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 斷言mockFn執行後返回100
expect(mockFn(10, 10)).toBe(100);
})
test('測試jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 斷言mockFn通過await關鍵字執行後返回值爲default
expect(result).toBe('default');
// 斷言mockFn調用後返回的是Promise對象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
上面的代碼是jest.fn()
提供的幾個常用的API和斷言語句,下面我們在src/fetch.js
文件中寫一些被測試代碼,以更加接近業務的方式來理解Mock函數的實際應用。
被測試代碼中依賴了
axios
這個常用的請求庫和JSONPlaceholder
這個上篇文章中提到免費的請求接口�,請先在shell
中執行npm install axios --save
安裝依賴,。
// fetch.js
import axios from 'axios';
export default {
async fetchPostsList(callback) {
return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
return callback(res.data);
})
}
}
我們在fetch.js
中封裝了一個fetchPostsList
方法,該方法請求了JSONPlaceholder
提供的接口,並通過傳入的回調函數返回處理過的返回值。如果我們想測試該接口能夠被正常請求,只需要捕獲到傳入的回調函數能夠被正常的調用即可。下面是functions.test.js
中的測試的代碼。
import fetch from '../src/fetch.js'
test('fetchPostsList中的回調函數應該能夠被調用', async () => {
expect.assertions(1);
let mockFn = jest.fn();
await fetch.fetchPostsList(mockFn);
// 斷言mockFn被調用
expect(mockFn).toBeCalled();
})
2. jest.mock()
fetch.js
文件夾中封裝的請求方法可能我們在其他模塊被調用的時候,並不需要進行實際的請求(請求方法已經通過單側或需要該方法返回非真實數據)。此時,使用jest.mock()
去mock整個模塊是十分有必要的。
下面我們在src/fetch.js
的同級目錄下創建一個src/events.js
。
// events.js
import fetch from './fetch';
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called!');
// do something
});
}
}
functions.test.js
中的測試代碼如下:
// functions.test.js
import events from '../src/events';
import fetch from '../src/fetch';
jest.mock('../src/fetch.js');
test('mock 整個 fetch.js模塊', async () => {
expect.assertions(2);
await events.getPostList();
expect(fetch.fetchPostsList).toHaveBeenCalled();
expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
});
在測試代碼中我們使用了jest.mock('../src/fetch.js')
去mock整個fetch.js
模塊。如果註釋掉這行代碼,執行測試腳本時會出現以下報錯信息
從這個報錯中,我們可以總結出一個重要的結論:
在jest中如果想捕獲函數的調用情況,則該函數必須被mock或者spy!
3. jest.spyOn()
jest.spyOn()
方法同樣創建一個mock函數,但是該mock函數不僅能夠捕獲函數的調用情況,還可以正常的執行被spy的函數。實際上,jest.spyOn()
是jest.fn()
的語法糖,它創建了一個和被spy的函數具有相同內部代碼的mock函數。
上圖是之前jest.mock()
的示例代碼中的正確執行結果的截圖,從shell腳本中可以看到console.log('fetchPostsList be called!');
這行代碼並沒有在shell中被打印,這是因爲通過jest.mock()
後,模塊內的方法是不會被jest所實際執行的。這時我們就需要使用jest.spyOn()
。
// functions.test.js
import events from '../src/events';
import fetch from '../src/fetch';
test('使用jest.spyOn()監控fetch.fetchPostsList被正常調用', async() => {
expect.assertions(2);
const spyFn = jest.spyOn(fetch, 'fetchPostsList');
await events.getPostList();
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(1);
})
執行npm run test
後,可以看到shell中的打印信息,說明通過jest.spyOn()
,fetchPostsList
被正常的執行了。
4. 總結
這篇文章中我們介紹了jest.fn()
,jest.mock()
和jest.spyOn()
來創建mock函數,通過mock函數我們可以通過以下三個特性去更好的編寫我們的測試代碼:
- 捕獲函數調用情況
- 設置函數返回值
- 改變函數的內部實現
在實際項目的單元測試中,jest.fn()
常被用來進行某些有回調函數的測試;jest.mock()
可以mock整個模塊中的方法,當某個模塊已經被單元測試100%覆蓋時,使用jest.mock()
去mock該模塊,節約測試時間和測試的冗餘度是十分必要;當需要測試某些必須被完整執行的方法時,常常需要使用jest.spyOn()
。這些都需要開發者根據實際的業務代碼靈活選擇。