寫在前面:
在寫單元測試的時候有一個最重要的步驟就是Mock,我們通常會根據接口來Mock接口的實現,比如你要測試某個class中的某個方法,而這個方法又依賴了外部的一些接口的實現,從單元測試的角度來說我只關心我測試的方法的內部邏輯,我並不關注與當前class本身依賴的實現,所以我們通常會Mock掉依賴接口的返回,因為我們的測試重點在於特定的方法,所以在Jest中同樣提供了Mock的功能,本節主要介紹Jest的Mock Function的功能。
Jest中的Mock Function
Mock 函數可以輕松地測試代碼之間的連接——這通過擦除函數的實際實現,捕獲對函數的調用 ( 以及在這些調用中傳遞的參數) ,在使用 new
實例化時捕獲構造函數的實例,或允許測試時配置返回值的形式來實現。Jest中有兩種方式的Mock Function,一種是利用Jest提供的Mock Function創建,另外一種是手動創建來覆寫本身的依賴實現。
假設我們要測試函數 forEach
的內部實現,這個函數為傳入的數組中的每個元素調用一個回調函數,代碼如下:
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } }
為了測試此函數,我們可以使用一個 mock 函數,然后檢查 mock 函數的狀態來確保回調函數如期調用。
const mockCallback = jest.fn(); forEach([0, 1], mockCallback); // 此模擬函數被調用了兩次 expect(mockCallback.mock.calls.length).toBe(2); // 第一次調用函數時的第一個參數是 0 expect(mockCallback.mock.calls[0][0]).toBe(0); // 第二次調用函數時的第一個參數是 1 expect(mockCallback.mock.calls[1][0]).toBe(1);
幾乎所有的Mock Function都帶有 .mock的屬性,它保存了此函數被調用的信息。 .mock
屬性還追蹤每次調用時 this
的值,所以也讓檢視 this 的值成為可能:
const myMock = jest.fn(); const a = new myMock(); const b = {}; const bound = myMock.bind(b); bound(); console.log(myMock.mock.instances);
在測試中,需要對函數如何被調用,或者實例化做斷言時,這些 mock 成員變量很有幫助意義︰
// 這個函數只調用一次 expect(someMockFunction.mock.calls.length).toBe(1); // 這個函數被第一次調用時的第一個 arg 是 'first arg' expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); // 這個函數被第一次調用時的第二個 arg 是 'second arg' expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); // 這個函數被實例化兩次 expect(someMockFunction.mock.instances.length).toBe(2); // 這個函數被第一次實例化返回的對象中,有一個 name 屬性,且被設置為了 'test’ expect(someMockFunction.mock.instances[0].name).toEqual('test');
Mock 函數也可以用於在測試期間將測試值注入您的代碼︰
const myMock = jest.fn(); console.log(myMock()); // > undefined myMock .mockReturnValueOnce(10) .mockReturnValueOnce('x') .mockReturnValue(true); console.log(myMock(), myMock(), myMock(), myMock());
用於函數連續傳遞風格(CPS)的代碼中時,Mock 函數也非常有效。 以這種風格編寫的代碼有助於避免那種需要通過復雜的中間值,來重建他們在真實組件的行為,這有利於在它們被調用之前將值直接注入到測試中。
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] ]
大多數現實世界的例子實際上都涉及到將一個被依賴的組件上使用 mock 函數替代並進行配置,這在技術上(和上面的描述)是相同的。 在這些情況下,盡量避免在非真正想要進行測試的任何函數內實現邏輯。
有些情況下超越指定返回值的功能是有用的,並且全面替換了模擬函數的實現。
const myMockFn = jest.fn(cb => cb(null, true)); myMockFn((err, val) => console.log(val)); // > 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; }), };
你也可以給你的Mock Function起一個准確的名字,這樣有助於你在測試錯誤的時候在輸出窗口定位到具體的Function
const myMockFn = jest .fn() .mockReturnValue('default') .mockImplementation(scalar => 42 + scalar) .mockName('add42');
最后,為了更簡單地說明如何調用mock函數,我們為您添加了一些自定義匹配器函數:
// 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).toContain([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.mock.getMockName()).toBe('a mock name');
寫在最后:
本文只是簡單的介紹了Mock Function的功能,更完整的匹配器列表,請查閱 參考文檔。
系列教程:
1. 前端測試框架Jest系列教程 -- Matchers(匹配器)
2.前端測試框架Jest系列教程 -- Asynchronous(測試異步代碼)