模擬函數(shù)通過擦去真正的函數(shù)實現(xiàn),捕獲函數(shù)調(diào)用(調(diào)用傳參),當(dāng)使用 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 返回的值,當(dāng) mockReturnValueOnce 調(diào)用次數(shù)結(jié)束后,默認(rèn)返回 mockReturnValue 的值
對應(yīng)一個持續(xù)傳遞的函數(shù)(forEach,filter)來說,在代碼里面使用 mock 函數(shù)是非常有效的。這樣就可以并不用去關(guān)注行為,而關(guān)注傳入的值是否正確。
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ù)并對其進(jìn)行配置,但是技術(shù)是相同的。在這些情況下,盡量避免在沒有直接測試的函數(shù)中實現(xiàn)邏輯。
Mocking 模塊
假設(shè)我們有一個 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')返回一個偽響應(yīng)。
// 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
當(dāng)您需要定義從另一個模塊創(chuàng)建的模擬函數(shù)的默認(rèn)實現(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
當(dāng)您需要重新創(chuàng)建模擬函數(shù)的復(fù)雜行為,以便多個函數(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
當(dāng)模擬函數(shù)運(yùn)行完用mockImplementationOnce定義的實現(xiàn)時,它將使用jest.fn()默認(rèn)的實現(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'
我們的方法通常是典型的鏈?zhǔn)剑ㄒ虼顺37祷?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ù)進(jìn)行的所有調(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 當(dāng) 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)用返回??梢员绘溄樱╟hained)調(diào)用,以便對模擬函數(shù)的連續(xù)調(diào)用返回不同的值。當(dāng)不再使用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
});