跳至主要內容
版本:29.7

模擬函式

模擬函式可讓您透過消除函式的實際執行、擷取對函式的呼叫(以及在這些呼叫中傳遞的參數)、擷取使用 new 建立實例時的建構函式實例,以及允許在測試時設定回傳值,來測試程式碼之間的連結。

有兩種模擬函式的方法:建立一個模擬函式在測試程式碼中使用,或撰寫一個 手動模擬 來覆寫模組相依性。

使用模擬函式

假設我們要測試函式 forEach 的實作,它會針對所提供陣列中的每個項目呼叫回呼函式。

forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}

若要測試此函式,我們可以使用模擬函式,並檢查模擬狀態,以確保呼叫回呼函式時符合預期。

forEach.test.js
const forEach = require('./forEach');

const mockCallback = jest.fn(x => 42 + x);

test('forEach mock function', () => {
forEach([0, 1], mockCallback);

// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});

.mock 屬性

所有模擬函式都有這個特殊的 .mock 屬性,其中會儲存函式呼叫方式和函式傳回值的資料。.mock 屬性也會追蹤每次呼叫的 this 值,因此也可以檢查這個值

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]

這些模擬成員在測試中非常有用,可以確認這些函式如何被呼叫、實例化或傳回什麼值

// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(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');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// 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).toBe('test');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

模擬傳回值

模擬函式也可以用於在測試期間將測試值注入您的程式碼中

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

模擬函式在使用函式延續傳遞樣式的程式碼中也非常有效。以這種樣式撰寫的程式碼有助於避免需要複雜的 stub,這些 stub 會重新建立它們所取代的實際元件的行為,而改為在使用之前直接將值注入測試中。

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[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

大多數實際範例實際上都涉及取得相依元件上的模擬函式並設定它,但技術是相同的。在這些情況下,請盡量避免在任何未直接測試的函式內實作邏輯。

模擬模組

假設我們有一個類別會從我們的 API 中擷取使用者。該類別使用 axios 來呼叫 API,然後傳回包含所有使用者的 data 屬性

users.js
import axios from 'axios';

class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}

export default Users;

現在,為了測試此方法而不用實際存取 API(從而建立緩慢且脆弱的測試),我們可以使用 jest.mock(...) 函式自動模擬 axios 模組。

模擬模組後,我們可以提供 .getmockResolvedValue,傳回我們希望測試用於斷言的資料。實際上,我們表示我們希望 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));
});

模擬部分

模組的子集可以被模擬,而模組的其餘部分可以保留它們的實際實作

foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');

//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});

test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();

expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

模擬實作

儘管如此,在某些情況下,超越指定回傳值並全面取代模擬函式的實作是有用的。這可以使用 jest.fn 或模擬函式上的 mockImplementationOnce 方法來完成。

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

當您需要定義從另一個模組建立的模擬函式的預設實作時,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

當您需要重新建立模擬函式的複雜行為,使得多個函式呼叫會產生不同的結果時,請使用 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

當模擬函式用盡使用 mockImplementationOnce 定義的實作時,它將執行使用 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'

對於通常會鏈結(因此總是需要回傳 this)的方法,我們有一個糖衣 API,以 .mockReturnThis() 函式的形式簡化這個過程,它也存在於所有模擬中

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

模擬名稱

您可以選擇為您的模擬函式提供一個名稱,它將顯示在測試錯誤輸出中,而不是 'jest.fn()'。如果您想要快速識別測試輸出中報告錯誤的模擬函式,請使用 .mockName()

const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');

自訂比對器

最後,為了減少斷言模擬函式如何被呼叫的要求,我們為您新增了一些自訂比對器函式

// 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 屬性的常見形式的糖衣。如果您比較喜歡這樣做,或者如果您需要做更具體的事情,您隨時可以自己手動執行此操作

// 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');

有關比對器的完整清單,請查看 參考文件