1. 程式人生 > >前端測試框架Jest系列教程 -- Mock Functions

前端測試框架Jest系列教程 -- Mock Functions

gpo ret his 教程 items add cal snap col

寫在前面:

  在寫單元測試的時候有一個最重要的步驟就是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的功能,更完整的匹配器列表,請查閱 參考文檔。

前端測試框架Jest系列教程 -- Mock Functions