2022年3月5日 星期六

測試套件 Jest mock 基礎概念及 spyOn, fn 範例程式

當測試的對象有使用其他引用(import)時,我們希望能做為控制變因,設法聚焦測試的對象。Jest是常用的js單元測試套件,我們會 mock 方法spyOnfn來達成這個目的,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);
驗證的部分,toHaveBeenCalledTimestoHaveBeenCalledWith這兩個還有另一個寫法:
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]);

參考資料: