模擬(?mock
?)函數(shù)允許你測(cè)試代碼之間的連接——實(shí)現(xiàn)方式包括:擦除函數(shù)的實(shí)際實(shí)現(xiàn)、捕獲對(duì)函數(shù)的調(diào)用 ( 以及在這些調(diào)用中傳遞的參數(shù)) 、在使用 ?new
?實(shí)例化時(shí)捕獲構(gòu)造函數(shù)的實(shí)例、允許測(cè)試時(shí)配置返回值。
有兩種方法可以模擬函數(shù):要么在測(cè)試代碼中創(chuàng)建一個(gè)模擬函數(shù),要么編寫(xiě)一個(gè) 手動(dòng)模擬 來(lái)覆蓋模塊依賴(lài)。
假設(shè)我們要測(cè)試函數(shù) ?forEach
?的內(nèi)部實(shí)現(xiàn),這個(gè)函數(shù)為傳入的數(shù)組中的每個(gè)元素調(diào)用一次回調(diào)函數(shù)。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
為了測(cè)試此函數(shù),我們可以使用一個(gè)模擬函數(shù),然后檢查模擬函數(shù)的狀態(tài)來(lái)確?;卣{(diào)函數(shù)如期調(diào)用。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 此 mock 函數(shù)被調(diào)用了兩次
expect(mockCallback.mock.calls.length).toBe(2);
// 第一次調(diào)用函數(shù)時(shí)的第一個(gè)參數(shù)是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 第二次調(diào)用函數(shù)時(shí)的第一個(gè)參數(shù)是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 第一次函數(shù)調(diào)用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
所有的模擬函數(shù)都有這個(gè)特殊的 ?.mock
?屬性,它保存了關(guān)于此函數(shù)如何被調(diào)用、調(diào)用時(shí)的返回值的信息。 ?.mock
? 屬性還追蹤每次調(diào)用時(shí) ?this
?的值,所以我們同樣可以也檢視(inspect) ?this
?:
const myMock = jest.fn();
const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();
console.log(myMock.mock.instances);
// > [ <a>, <b> ]
這些模擬成員變量在測(cè)試中非常有用,用于說(shuō)明這些函數(shù)是如何被調(diào)用、實(shí)例化或返回的:
// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1);
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');
// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);
// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');
模擬函數(shù)也可以用于在測(cè)試期間將測(cè)試值注入代碼︰
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
在函數(shù)連續(xù)傳遞風(fēng)格(?functional continuation-passing style
?)的代碼中時(shí),模擬函數(shù)也非常有效。 以這種代碼風(fēng)格有助于避免復(fù)雜的中間操作,便于直觀表現(xiàn)組件的真實(shí)意圖,這有利于在它們被調(diào)用之前,將值直接注入到測(cè)試中。
const filterTestFn = jest.fn();
// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(num => filterTestFn(num));
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]
大多數(shù)現(xiàn)實(shí)世界例子中,實(shí)際是在依賴(lài)的組件上配一個(gè)模擬函數(shù)并配置它,但手法是相同的。 在這些情況下,盡量避免在非真正想要進(jìn)行測(cè)試的任何函數(shù)內(nèi)實(shí)現(xiàn)邏輯。
假定有個(gè)從 API 獲取用戶(hù)的類(lèi)。 該類(lèi)用 ?axios
? 調(diào)用 API 然后返回 ?data
?,其中包含所有用戶(hù)的屬性:
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
現(xiàn)在,為測(cè)試該方法而不實(shí)際調(diào)用 API (使測(cè)試緩慢與脆弱),我們可以用 ?jest.mock(...)
函數(shù)自動(dòng)模擬 axios 模塊。
一旦模擬模塊,我們可為? .get
? 提供一個(gè) ?mockResolvedValue
?,它會(huì)返回假數(shù)據(jù)用于測(cè)試。 實(shí)際上,我們想讓 ?axios.get(‘/users.json’)
? 有個(gè)假的 ?response
?。
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
盡管如此,在某些情況下,超越指定返回值的能力并完全替換模擬函數(shù)的實(shí)現(xiàn)是有用的。這可以通過(guò)?jest.fn
?或?mockImplementationOnce
?模擬函數(shù)上的方法來(lái)完成。
const myMockFn = jest.fn(cb => cb(null, true));
myMockFn((err, val) => console.log(val));
// > true
?mockImplementation
?
當(dāng)你需要定義從另一個(gè)模塊創(chuàng)建的模擬函數(shù)的默認(rèn)實(shí)現(xiàn)時(shí),該方法很有用:
// foo.js
module.exports = function () {
// some implementation;
};
// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
當(dāng)你需要重新創(chuàng)建模擬函數(shù)的復(fù)雜行為以致多個(gè)函數(shù)調(diào)用產(chǎn)生不同的結(jié)果時(shí),請(qǐng)使用以下?mockImplementationOnce
?方法:
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false
當(dāng)模擬函數(shù)用完用?mockImplementationOnce
?定義的實(shí)現(xiàn)時(shí),它將執(zhí)行設(shè)置的默認(rèn)實(shí)現(xiàn)?jest.fn
?(如果已定義):
- const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'
對(duì)于具有通常鏈接的方法(因此總是需要返回this
?)的情況,我們有一個(gè)友好的 API 以?.mockReturnThis()
?函數(shù)的形式簡(jiǎn)化它,該函數(shù)也位于所有模擬中:
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};
你可以選擇為你的模擬函數(shù)提供一個(gè)名稱(chēng),該名稱(chēng)將在測(cè)試錯(cuò)誤輸出中顯示而不是“jest.fn()”。如果你希望能夠快速識(shí)別在測(cè)試輸出中報(bào)告錯(cuò)誤的模擬函數(shù),請(qǐng)使用此選項(xiàng)。
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');
最后,為了減少斷言如何調(diào)用模擬函數(shù)的要求,我們?yōu)槟闾砑恿艘恍┳远x匹配器函數(shù):
// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();
// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();
這些匹配器是檢查?.mock
?財(cái)產(chǎn)的常見(jiàn)形式的糖。如果這更符合你的習(xí)慣或者你需要做一些更具體的事情,你始終可以自己手動(dòng)執(zhí)行此操作:
// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);
// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. 它還會(huì)在名稱(chēng)上斷言。
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');
這些只是一部分,有關(guān)匹配器的完整列表,請(qǐng)查閱 參考文檔。
更多建議: