利用模擬物件進行互動測試 | 單元測試的藝術 第 3 版 | 閱讀筆記

「單元測試的藝術」讀書會 - 利用模擬物件進行互動測試 (The Art of Unit Testing, 3e - Interaction Testing Using Mock Objects) 閱讀筆記。

簡介

當成參數帶入

步驟:

  1. 在原本的函式增加一個新的參數,由這個參數帶入要呼叫的第三方函式。
  2. 在測試程式中,實作 mock function 並將其當成參數帶入函式。

優點:由於是當成參數帶入,降低呼叫者的負擔,降低呼叫者與第三方函式的耦合度。

舉例來說,checkValentinesDay 函式會檢查今天是否為情人節,若今天是 2 月 14 日,就回傳字串 情人節快樂,若不是則回傳 今天不是情人節。對於 checkValentinesDay 來說,取得今天的日期的函式 getToday 是一個外部依賴,若想確認 getToday 有沒有被正確呼叫,可以在測試程式中模擬取得日期的函式當成參數傳入 checkValentinesDay 函式中,然後來監控這個模擬,進而確認是否正確的呼叫 getToday

const checkValentinesDay = (getToday) => {
  const today = getToday();
  return today === '2/14' ? '情人節快樂' : '今天不是情人節';
};

由於 mockGetToday 有正確的被 checkValentinesDay,因此 today 的值符合預期,間接確認 getToday 會被正確呼叫。

describe('checkValentinesDay', () => {
  it('2/12 should not be Valentines Day', () => {
    let today = '';
    const mockGetToday = () => {
      today = '2/12';
      return today;
    };

    expect(checkValentinesDay(mockGetToday)).toBe('今天不是情人節');
    expect(today).toBe('2/12');
  });

  it('2/14 should be Valentines Day', () => {
    let today = '';
    const mockGetToday = () => {
      today = '2/14';
      return today;
    };

    expect(checkValentinesDay(mockGetToday)).toBe('情人節快樂');
    expect(today).toBe('2/14');
  });
});

模組抽象化

步驟:

優點:簡單。

缺點:需要手動 inject 和 reset 模組。

舉例來說,在先前模組注入中提到的例子,以下這段設定即是手動抽象畫介面與 inject 模組。

import bakeAllCakes, { bakeMatchaRoll } from './bakeUtils';

jest.mock('./bakeUtils', () => {
  const originalModule = jest.requireActual('./bakeUtils');

  return {
    __esModule: true,
    ...originalModule,
    default: jest
      .fn()
      .mockReturnValue('Chocolate Pudding and Matcha Roll are all baked.'),
  };
});

測試執行完畢後,清除模擬,還原為原本的模組。

afterEach(() => {
  jest.clearAllMocks();
});

帶入函式

物件導向的介面

Class-based Design with Constructor

利用 class 的方式實作,再透過 constructor 強制呼叫者提供參數。

改寫前面的例子 checkValentinesDay 函式,將取得今天日期的函式注入到建構子 (constructor) 中,並且在 checkValentinesDay 函式中呼叫這個函式,最後確認 today 的值是否正確。

class ValentinesDayChecker {
  constructor(getToday) {
    this.getToday = getToday;
    this.today = '';
  }

  checkValentinesDay() {
    this.today = this.getToday();
    return this.today === '2/14' ? '情人節快樂' : '今天不是情人節';
  }
}
describe('checkValentinesDay', () => {
  it('2/12 should not be Valentines Day', () => {
    const getToday = () => '2/12';
    const checker = new ValentinesDayChecker(getToday);
    expect(checker.checkValentinesDay()).toBe('今天不是情人節');
    expect(checker.today).toBe('2/12');
  });

  it('2/14 should be Valentines Day', () => {
    const getToday = () => '2/14';
    const checker = new ValentinesDayChecker(getToday);
    expect(checker.checkValentinesDay()).toBe('情人節快樂');
    expect(checker.today).toBe('2/14');
  });
});

Interface-based Design

利用介面的方式實作,透過介面來定義依賴的行為,並注入依賴。

優點:有彈性、耦合度低,且可以避免錯誤。

interface TimeProvider {
  getToday: () => string;
  today: string;
}
class FakeTimeProvider implements TimeProvider {
  constructor(private fakeDay: string) {}

  getToday() {
    this.today = this.fakeDay;
    return this.fakeDay;
  }
}

class ValentinesDayChecker {
  constructor(private timeProvider: TimeProvider) {}

  checkValentinesDay() {
    const today = this.timeProvider.getToday();
    return today === '2/14' ? '情人節快樂' : '今天不是情人節';
  }
}
describe('checkValentinesDay', () => {
  it('2/12 should not be Valentines Day', () => {
    const checker = new ValentinesDayChecker(new FakeTimeProvider('2/12'));
    expect(checker.checkValentinesDay()).toBe('今天不是情人節');
    expect(checker.today).toBe('2/14');
  });

  it('2/14 should be Valentines Day', () => {
    const checker = new ValentinesDayChecker(new FakeTimeProvider('2/14'));
    expect(checker.checkValentinesDay()).toBe('情人節快樂');
    expect(checker.today).toBe('2/14');
  });
});

注意:

參考資料


The Art of Unit Testing Unit Test front end testing Jest 單元測試 自動化測試 閱讀筆記 讀書會 sharing