Jest 模擬函數(shù)

2021-09-18 11:54 更新

模擬(?mock?)函數(shù)允許你測(cè)試代碼之間的連接——實(shí)現(xiàn)方式包括:擦除函數(shù)的實(shí)際實(shí)現(xiàn)、捕獲對(duì)函數(shù)的調(diào)用 ( 以及在這些調(diào)用中傳遞的參數(shù)) 、在使用 ?new ?實(shí)例化時(shí)捕獲構(gòu)造函數(shù)的實(shí)例、允許測(cè)試時(shí)配置返回值。

有兩種方法可以模擬函數(shù):要么在測(cè)試代碼中創(chuàng)建一個(gè)模擬函數(shù),要么編寫(xiě)一個(gè) 手動(dòng)模擬 來(lái)覆蓋模塊依賴(lài)。

使用模擬函數(shù)

假設(shè)我們要測(cè)試函數(shù) ?forEach ?的內(nèi)部實(shí)現(xiàn),這個(gè)函數(shù)為傳入的數(shù)組中的每個(gè)元素調(diào)用一次回調(diào)函數(shù)。

  1. function forEach(items, callback) {
  2. for (let index = 0; index < items.length; index++) {
  3. callback(items[index]);
  4. }
  5. }

為了測(cè)試此函數(shù),我們可以使用一個(gè)模擬函數(shù),然后檢查模擬函數(shù)的狀態(tài)來(lái)確?;卣{(diào)函數(shù)如期調(diào)用。

  1. const mockCallback = jest.fn(x => 42 + x);
  2. forEach([0, 1], mockCallback);
  3. // 此 mock 函數(shù)被調(diào)用了兩次
  4. expect(mockCallback.mock.calls.length).toBe(2);
  5. // 第一次調(diào)用函數(shù)時(shí)的第一個(gè)參數(shù)是 0
  6. expect(mockCallback.mock.calls[0][0]).toBe(0);
  7. // 第二次調(diào)用函數(shù)時(shí)的第一個(gè)參數(shù)是 1
  8. expect(mockCallback.mock.calls[1][0]).toBe(1);
  9. // 第一次函數(shù)調(diào)用的返回值是 42
  10. expect(mockCallback.mock.results[0].value).toBe(42);

.mock 屬性

所有的模擬函數(shù)都有這個(gè)特殊的 ?.mock?屬性,它保存了關(guān)于此函數(shù)如何被調(diào)用、調(diào)用時(shí)的返回值的信息。 ?.mock? 屬性還追蹤每次調(diào)用時(shí) ?this?的值,所以我們同樣可以也檢視(inspect) ?this?:

  1. const myMock = jest.fn();
  2. const a = new myMock();
  3. const b = {};
  4. const bound = myMock.bind(b);
  5. bound();
  6. console.log(myMock.mock.instances);
  7. // > [ <a>, <b> ]

這些模擬成員變量在測(cè)試中非常有用,用于說(shuō)明這些函數(shù)是如何被調(diào)用、實(shí)例化或返回的:

  1. // The function was called exactly once
  2. expect(someMockFunction.mock.calls.length).toBe(1);
  3. // The first arg of the first call to the function was 'first arg'
  4. expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
  5. // The second arg of the first call to the function was 'second arg'
  6. expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
  7. // The return value of the first call to the function was 'return value'
  8. expect(someMockFunction.mock.results[0].value).toBe('return value');
  9. // This function was instantiated exactly twice
  10. expect(someMockFunction.mock.instances.length).toBe(2);
  11. // The object returned by the first instantiation of this function
  12. // had a `name` property whose value was set to 'test'
  13. expect(someMockFunction.mock.instances[0].name).toEqual('test');

模擬的返回值

模擬函數(shù)也可以用于在測(cè)試期間將測(cè)試值注入代碼︰

  1. const myMock = jest.fn();
  2. console.log(myMock());
  3. // > undefined
  4. myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
  5. console.log(myMock(), myMock(), myMock(), myMock());
  6. // > 10, 'x', true, true

在函數(shù)連續(xù)傳遞風(fēng)格(?functional continuation-passing style?)的代碼中時(shí),模擬函數(shù)也非常有效。 以這種代碼風(fēng)格有助于避免復(fù)雜的中間操作,便于直觀表現(xiàn)組件的真實(shí)意圖,這有利于在它們被調(diào)用之前,將值直接注入到測(cè)試中。

  1. const filterTestFn = jest.fn();
  2. // Make the mock return `true` for the first call,
  3. // and `false` for the second call
  4. filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
  5. const result = [11, 12].filter(num => filterTestFn(num));
  6. console.log(result);
  7. // > [11]
  8. console.log(filterTestFn.mock.calls);
  9. // > [ [11], [12] ]

大多數(shù)現(xiàn)實(shí)世界例子中,實(shí)際是在依賴(lài)的組件上配一個(gè)模擬函數(shù)并配置它,但手法是相同的。 在這些情況下,盡量避免在非真正想要進(jìn)行測(cè)試的任何函數(shù)內(nèi)實(shí)現(xiàn)邏輯。

模擬模塊

假定有個(gè)從 API 獲取用戶(hù)的類(lèi)。 該類(lèi)用 ?axios? 調(diào)用 API 然后返回 ?data?,其中包含所有用戶(hù)的屬性:

  1. // users.js
  2. import axios from 'axios';
  3. class Users {
  4. static all() {
  5. return axios.get('/users.json').then(resp => resp.data);
  6. }
  7. }
  8. export default Users;

現(xiàn)在,為測(cè)試該方法而不實(shí)際調(diào)用 API (使測(cè)試緩慢與脆弱),我們可以用 ?jest.mock(...)函數(shù)自動(dòng)模擬 axios 模塊。

一旦模擬模塊,我們可為? .get? 提供一個(gè) ?mockResolvedValue ?,它會(huì)返回假數(shù)據(jù)用于測(cè)試。 實(shí)際上,我們想讓 ?axios.get(‘/users.json’)? 有個(gè)假的 ?response?。

  1. // users.test.js
  2. import axios from 'axios';
  3. import Users from './users';
  4. jest.mock('axios');
  5. test('should fetch users', () => {
  6. const users = [{name: 'Bob'}];
  7. const resp = {data: users};
  8. axios.get.mockResolvedValue(resp);
  9. // or you could use the following depending on your use case:
  10. // axios.get.mockImplementation(() => Promise.resolve(resp))
  11. return Users.all().then(data => expect(data).toEqual(users));
  12. });

模擬實(shí)現(xiàn)

盡管如此,在某些情況下,超越指定返回值的能力并完全替換模擬函數(shù)的實(shí)現(xiàn)是有用的。這可以通過(guò)?jest.fn?或?mockImplementationOnce?模擬函數(shù)上的方法來(lái)完成。

  1. const myMockFn = jest.fn(cb => cb(null, true));
  2. myMockFn((err, val) => console.log(val));
  3. // > true

?mockImplementation?

當(dāng)你需要定義從另一個(gè)模塊創(chuàng)建的模擬函數(shù)的默認(rèn)實(shí)現(xiàn)時(shí),該方法很有用:

  1. // foo.js
  2. module.exports = function () {
  3. // some implementation;
  4. };
  5. // test.js
  6. jest.mock('../foo'); // this happens automatically with automocking
  7. const foo = require('../foo');
  8. // foo is a mock function
  9. foo.mockImplementation(() => 42);
  10. foo();
  11. // > 42

當(dāng)你需要重新創(chuàng)建模擬函數(shù)的復(fù)雜行為以致多個(gè)函數(shù)調(diào)用產(chǎn)生不同的結(jié)果時(shí),請(qǐng)使用以下?mockImplementationOnce?方法:

  1. const myMockFn = jest
  2. .fn()
  3. .mockImplementationOnce(cb => cb(null, true))
  4. .mockImplementationOnce(cb => cb(null, false));
  5. myMockFn((err, val) => console.log(val));
  6. // > true
  7. myMockFn((err, val) => console.log(val));
  8. // > false

當(dāng)模擬函數(shù)用完用?mockImplementationOnce?定義的實(shí)現(xiàn)時(shí),它將執(zhí)行設(shè)置的默認(rèn)實(shí)現(xiàn)?jest.fn?(如果已定義):

  1. const myMockFn = jest
  2. .fn(() => 'default')
  3. .mockImplementationOnce(() => 'first call')
  4. .mockImplementationOnce(() => 'second call');
  5. console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
  6. // > 'first call', 'second call', 'default', 'default'

對(duì)于具有通常鏈接的方法(因此總是需要返回this?)的情況,我們有一個(gè)友好的 API 以?.mockReturnThis()?函數(shù)的形式簡(jiǎn)化它,該函數(shù)也位于所有模擬中:

  1. const myObj = {
  2. myMethod: jest.fn().mockReturnThis(),
  3. };
  4. // is the same as
  5. const otherObj = {
  6. myMethod: jest.fn(function () {
  7. return this;
  8. }),
  9. };

模擬名稱(chēng)

你可以選擇為你的模擬函數(shù)提供一個(gè)名稱(chēng),該名稱(chēng)將在測(cè)試錯(cuò)誤輸出中顯示而不是“jest.fn()”。如果你希望能夠快速識(shí)別在測(cè)試輸出中報(bào)告錯(cuò)誤的模擬函數(shù),請(qǐng)使用此選項(xiàng)。

  1. const myMockFn = jest
  2. .fn()
  3. .mockReturnValue('default')
  4. .mockImplementation(scalar => 42 + scalar)
  5. .mockName('add42');

自定義匹配器

最后,為了減少斷言如何調(diào)用模擬函數(shù)的要求,我們?yōu)槟闾砑恿艘恍┳远x匹配器函數(shù):

  1. // The mock function was called at least once
  2. expect(mockFunc).toHaveBeenCalled();
  3. // The mock function was called at least once with the specified args
  4. expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
  5. // The last call to the mock function was called with the specified args
  6. expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
  7. // All calls and the name of the mock is written as a snapshot
  8. expect(mockFunc).toMatchSnapshot();

這些匹配器是檢查?.mock?財(cái)產(chǎn)的常見(jiàn)形式的糖。如果這更符合你的習(xí)慣或者你需要做一些更具體的事情,你始終可以自己手動(dòng)執(zhí)行此操作:

  1. // The mock function was called at least once
  2. expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
  3. // The mock function was called at least once with the specified args
  4. expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
  5. // The last call to the mock function was called with the specified args
  6. expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  7. arg1,
  8. arg2,
  9. ]);
  10. // The first arg of the last call to the mock function was `42`
  11. // (note that there is no sugar helper for this specific of an assertion)
  12. expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
  13. // A snapshot will check that a mock was invoked the same number of times,
  14. // in the same order, with the same arguments. 它還會(huì)在名稱(chēng)上斷言。
  15. expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
  16. expect(mockFunc.getMockName()).toBe('a mock name');

這些只是一部分,有關(guān)匹配器的完整列表,請(qǐng)查閱 參考文檔。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)