當測試的對象有使用其他引用(import)時,我們希望能做為控制變因,設法聚焦測試的對象。Jest是常用的js單元測試套件,我們會 mock 方法spyOn、fn來達成這個目的,Jest 安裝可以參考「[JavaScript] 安裝Jest單元測試」,底下說明幾種常用的情境。
jest.spyOn(object, methodName)
1. [基本用法] 監聽
單純測試某個method被呼叫的次數,或(官方說明),舉例下面範例是輸入兩個數字,並隨機輸出這個範圍內的數字。// getRandonInt.js export const getRandonInt = (min, max) => ( Math.floor(Math.random(max) * (max - min) + min) );例如 Math.floor
jest.spyOn(Math, 'floor')同理 Math.random ,所以可以將測試寫成:
import {getRandonInt} from './getRandonInt.js'; test('Should get random number between input min and max', () => { const spyFloor = jest.spyOn(Math, 'floor'); const spyRandom = jest.spyOn(Math, 'random'); const result = getRandonInt(1, 100); expect(result).toBeGreaterThanOrEqual(1); expect(result).toBeLessThanOrEqual(100); expect(spyFloor).toHaveBeenCalledTimes(1); expect(spyRandom).toHaveBeenCalledTimes(1); expect(spyRandom).toHaveBeenCalledWith(100); });
因此可看到我們監聽的兩個method,第一 toHaveBeenCalledTimes 分別被呼叫的次數,及 toHaveBeenCalledWith 傳入的參數。
2. [進階] 替換回傳值
改變某個method的回傳值,以下面的範例是希望輸入一個值後,可以隨機產生一個數字並得到兩者相乘的結果// multiplyNumber.js export const multiplyNumber = num => ( Math.random(10) * num );如果希望控制隨機產生的數字,可以使用 mockImplementation
import {multiplyNumber} from './multiplyNumber.js'; test('Should get the multiple of random number and input num', () => { const spyRandom = jest.spyOn(Math, 'random') .mockImplementation(() => 5); expect(Math.random(10)).toBe(5); expect(multiplyNumber(1000)).toBe(5000); spyRandom.mockRestore(); expect(Math.random(10)).not.toBe(5); });可以理解成實際我們已經將 Math.random 變成:
Math['random'] = () => 5; // you could definitely get parameter when method was called // so implementation would be like ... // Math['random'] = a => (a + 1);
mockRestore()可以恢復原本method的功能,因此直到我們呼叫 mockRestore()為止,無論我們呼叫幾次 Math.random ,都會按照我們的設定回傳數字 5。
假設希望這個設定是一次性的,則可以改用 mockImplementationOnce,這個寫法比較不容易出錯,是比較推薦的用法。
jest.spyOn(Math, 'random') .mockImplementationOnce(() => 5); .mockImplementationOnce(() => 20); expect(Math.random(10)).toBe(5); expect(Math.random(10)).toBe(20);
jest.fn(implementation)
用法和 spyOn非常類似,可以用來驗證呼叫次數及傳入參數等
const mockFn = jest.fn(); mockFn(100); expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith(100);驗證的部分,toHaveBeenCalledTimes和toHaveBeenCalledWith這兩個還有另一個寫法:
expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn.mock.calls.length).toBe(1); expect(mockFn).toHaveBeenCalledWith(100); expect(mockFn.mock.calls[0][0]).toBe(100);
calls是一個陣列,長度等同於呼叫的次數,陣列內每一個位置也都是一個陣列,每個位置則對應到呼叫時的參數內容。可以實際印出得到 calls = [[100]],所以calls[0][0]代表的是第一次呼叫時第一個參數是什麼
依照 multiplyNumber.js 的範例,可以使用fn達到同樣的效果,測試結束前也要記得呼叫mockRestore免得影響其他測試‧。
const spyRandom = jest.fn(() => 5); Math['random'] = spyRandom;
const spyRandom = jest.fn() .mockImplementation(() => 5); Math['random'] = spyRandom;
const spyRandom = jest.fn() .mockReturnValueOnce(5); Math['random'] = spyRandom;
jest.mock(module)
當測試有引入(import)其他模組、函式時,可以使用mock來替換回傳內容,我們稍微改寫剛剛的 multiplyNumber:
import {getRandonInt} from './combineArray.js'; export const multiplyNumber = (min, max, num) => ( getRandonInt(min, max) * num );
為了清楚此時的getRandonInt已經被替換掉,我們刻意重新命名,在測試裡將回傳值設定為數字 20
import {getRandonInt as mockGetRandonInt} from './getRandonInt'; import {multiplyNumber} from './multiplyNumber'; jest.mock('./getRandonInt'); test('Should get the multiple of random number and input num', () => { mockGetRandonInt.mockReturnValueOnce(20); expect(multiplyNumber(1, 2, 3)).toBe(60); expect(mockGetRandonInt).toHaveBeenCalledTimes(1); expect(mockGetRandonInt).toHaveBeenCalledWith(1, 2); });
另外可以利用requireActual可以取得原始的功能,當我們只需要部份取代時,可以藉此替換部分的內容,其餘皆按照原設定
const {getRandonInt} = jest.requireActual('../src/libs/combineArray'); mockGetRandonInt.mockImplementationOnce((a, b) => a + b); expect(getRandonInt(1, 2)).toBeLessThanOrEqual(2); expect(mockGetRandonInt(1, 2)).toBe(3); expect(mockGetRandonInt).toHaveBeenCalledTimes(1);
最後一個重點,建立和初始化一個class的物件時,可以透過instances來得到建立的物件,它是一個陣列,會依照建立順序放在對應的位置上。
const myMock = jest.fn(); const a = new myMock(); expect(a).toBe(myMock.mock.instances[0]);
參考資料:
- https://jestjs.io/docs/mock-functions
- https://jestjs.io/docs/mock-function-api
- https://jestjs.io/docs/es6-class-mocks
- https://jestjs.io/docs/jest-object