Jest - 模擬函數(shù) mock function

模擬函數(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
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容