模擬函數(shù)通過擦去真正的函數(shù)實現(xiàn)析苫,捕獲函數(shù)調(diào)用(調(diào)用傳參)萌狂,當使用 new
實例化的時候捕獲構(gòu)造函數(shù)鹃栽,并允許測試時配置返回值躏率,從而更簡單地測試代碼之間的鏈接,民鼓。
這有兩種方法可以模擬函數(shù):要么創(chuàng)建一個模擬函數(shù)用于測試代碼薇芝,要么編寫一個手動模擬來覆蓋模塊依賴項。
使用一個 mock function
想象我們正在測試 forEach
函數(shù)的實現(xiàn)丰嘉,該函數(shù)為數(shù)組中每個項調(diào)用回調(diào)函數(shù)恩掷。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
為了測試這個函數(shù),我們使用一個 mock 函數(shù)供嚎,并且檢查這個 mock 的狀態(tài)以確保回調(diào)如期望所調(diào)用。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 這個 mock 函數(shù)被調(diào)用了兩次
expect(mockCallback.mock.calls.length).toBe(2);
// 函數(shù)第一個調(diào)用的第一個參數(shù)是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 函數(shù)第二個調(diào)用的第一個參數(shù)是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 函數(shù)第一個調(diào)用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
.mock 屬性
所有的 mock 函數(shù)都有這個特殊的 .mock
屬性克滴,這個屬性里面存儲了函數(shù)如何被調(diào)用和函數(shù)返回什么的數(shù)據(jù)逼争。這個.mock
屬性也追蹤每個調(diào)用的this
值,因此也可以檢查這個值:
const myMock = jest.fn();
const a = new myMock(); // 使用 new 方法新建實例 a
const b = {};
const bound = myMock.bind(b); // 將 myMock 的上下文綁定到 b
bound();
console.log(myMock.mock.instances);
// > [ <a>, <b> ]
這些 mock 成員在測試中非常有用劝赔,可以斷言這些函數(shù)如何被調(diào)用誓焦、實例化或返回什么:
// 這個函數(shù)實際上被調(diào)用了一次
expect(someMockFunction.mock.calls.length).toBe(1);
// 函數(shù)第一次調(diào)用的第一個參數(shù)是 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// 函數(shù)第一次調(diào)用的第二個參數(shù)是 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// 函數(shù)第一次調(diào)用的返回值是 ’return value‘
expect(someMockFunction.mock.results[0].value).toBe('return value');
// 這個函數(shù)被實例化了兩次
expect(someMockFunction.mock.instances.length).toBe(2);
// 函數(shù)第一個實例化返回的對象有一個 'name' 屬性值是 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');
Mock 返回值
Mock 函數(shù)也可以用于在測試期間將測試值注入到代碼中:
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
// 優(yōu)先使用 mockReturnValueOnce 返回的值,當 mockReturnValueOnce 調(diào)用次數(shù)結(jié)束后着帽,默認返回 mockReturnValue 的值
對應一個持續(xù)傳遞的函數(shù)(forEach杂伟,filter)來說,在代碼里面使用 mock 函數(shù)是非常有效的仍翰。這樣就可以并不用去關注行為赫粥,而關注傳入的值是否正確。
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(filterTestFn);
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]
實際上予借,大多數(shù)實際示例都涉及到獲取依賴組件上的模擬函數(shù)并對其進行配置越平,但是技術是相同的。在這些情況下灵迫,盡量避免在沒有直接測試的函數(shù)中實現(xiàn)邏輯秦叛。
Mocking 模塊
假設我們有一個 class,從我們的 API 拉取用戶瀑粥。這個 class 使用 axios 去調(diào)用 API 挣跋,然后返回包含所有用的 data
屬性:
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
現(xiàn)在,為了在不實際碰到API的情況下測試這個方法(從而創(chuàng)建慢而脆弱的測試)狞换,我們可以使用jest.mock(…)
函數(shù)來自動模擬axios模塊避咆。
模擬模塊之后,我們可以為.get提供mockResolvedValue
哀澈,該值返回我們希望測試斷言的數(shù)據(jù)牌借。實際上,我們說的是希望axios.get('/users.json')
返回一個偽響應割按。
// 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));
});
Mock 實現(xiàn)
不過膨报,有些情況超出了指定返回值的能力,這時候替換 mock 函數(shù)的實現(xiàn)則非常有用适荣。這個可以使用 jest.fn
或者 mockImplementationOnce
方法 mock 函數(shù)實現(xiàn)现柠。
const myMockFn = jest.fn(cb => cb(null, true));
myMockFn((err, val) => console.log(val));
// > true
當您需要定義從另一個模塊創(chuàng)建的模擬函數(shù)的默認實現(xiàn)時,mockImplementation方法非常有用:
// 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
當您需要重新創(chuàng)建模擬函數(shù)的復雜行為弛矛,以便多個函數(shù)調(diào)用產(chǎn)生不同的結(jié)果時够吩,請使用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
當模擬函數(shù)運行完用mockImplementationOnce
定義的實現(xiàn)時,它將使用jest.fn()
默認的實現(xiàn)集(如果定義了):
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'
我們的方法通常是典型的鏈式(因此常常返回 this
)丈氓,我們有一個糖 API 可以簡化 this周循,它的形式是.mockReturnThis()
函數(shù)强法。
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function() {
return this;
}),
};
Mock 名字
您可以選擇為模擬函數(shù)提供一個名稱,它將在測試錯誤輸出中顯示湾笛,而不是“jest.fn()”饮怯。如果希望能夠快速識別模擬函數(shù),并報告測試輸出中的錯誤嚎研,請使用此方法蓖墅。
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).toBeCalled();
// The mock function was called at least once with the specified args
expect(mockFunc).toBeCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified args
expect(mockFunc).lastCalledWith(arg1, arg2);
// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();
這些匹配器實際上只是檢查.mock屬性的常見形式的糖论矾。如果更符合你的口味,或者你需要做一些更具體的事情杆勇,你可以自己動手做:
// 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. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');
參考
mockFn.mock.calls
包含對這個模擬函數(shù)所做的所有調(diào)用的調(diào)用參數(shù)的數(shù)組贪壳。數(shù)組中的每個項都是在調(diào)用期間傳遞的參數(shù)數(shù)組。
例如:一個 mock 函數(shù) f
被調(diào)用了兩次靶橱,第一次使用參數(shù) f('arg1', 'arg2')
寥袭,第二次使用參數(shù) f('arg3', 'arg4')
,將有一個 mock.calls
數(shù)組如下所示:
[['arg1', 'arg2'], ['arg3', 'arg4']];
mockFn.mock.results
一個數(shù)組关霸,包含對這個模擬函數(shù)進行的所有調(diào)用的結(jié)果传黄。這個數(shù)組中的每個條目都是一個對象,其中包含一個type
屬性和一個value
屬性队寇。type
的值如下:
-
'return'
- 指明這個調(diào)用完成后正常返回(return)膘掰。 -
'throw'
- 指明這個調(diào)用完成后拋出(throw)一個值。 -
'incomplete'
- 指明這個調(diào)用沒有完成佳遣。如果您從模擬函數(shù)本身或從模擬調(diào)用的函數(shù)中測試結(jié)果识埋,則會發(fā)生這種情況。
這個 value
屬性包含了一個拋出(throw)或返回(return)的值零渐。value
是 undefined 當 type === 'incomplete'
窒舟。
例如:一個 mock 函數(shù) f
被調(diào)用了三次,返回 'result1'
诵盼,然后拋出一個錯誤惠豺,最后返回 'result2'
,它的 mock.results
數(shù)組將如下所示:
[
{
type: 'return',
value: 'result1',
},
{
type: 'throw',
value: {
/* Error instance */
},
},
{
type: 'return',
value: 'result2',
},
];
mockFn.mock.instances
一個數(shù)組风宁,其中包含使用new從這個模擬函數(shù)實例化的所有對象實例洁墙。
例如:一個 mock 函數(shù)被實例化了兩次,mock.instances
數(shù)組如下:
const mockFn = jest.fn();
const a = new mockFn();
const b = new mockFn();
mockFn.mock.instance[0] === a; // true
mockFn.mock.instance[1] === b; // true
mockFn.mockReturnValue(value)
接受一個值戒财,該值將在調(diào)用模擬函數(shù)時返回热监。
const mock = jest.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43
mockFn.mockReturnValueOnce(value)
接受一個值,該值將為對模擬函數(shù)的一次調(diào)用返回饮寞⌒⒖福可以被鏈接(chained)調(diào)用列吼,以便對模擬函數(shù)的連續(xù)調(diào)用返回不同的值。當不再使用mockReturnValueOnce
值時疗琉,調(diào)用將返回mockReturnValue
指定的值冈欢。
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call');
// 'first call', 'second call', 'default', 'default'
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
mockFn.mockResolvedValue(value)
下面方法的語法糖函數(shù):
jest.fn().mockImplementation(() => Promise.resolve(value));
在異步測試 mock 異步函數(shù)特別有用:
test('async test', async () => {
const asyncMock = jest.fn().mockResolvedValue(43);
await asyncMock(); // 43
});
mockFn.mockResolvedValueOnce(value)
下面方法的語法糖:
jest.fn().mockImplementationOnce(() => Promise.resolve(value));
多次異步測試 resolve 不同的值非常有用:
test('async test', async () => {
const asyncMock = jest
.fn()
.mockResolvedValue('default')
.mockResolvedValueOnce('first call')
.mockResolvedValueOnce('second call');
await asyncMock(); // first call
await asyncMock(); // second call
await asyncMock(); // default
await asyncMock(); // default
});