91 Unit Testing 單元測試實戰操練營 筆記 - 寫測試的正確姿勢

參加 91 Unit Testing 單元測試實戰操練營的簡單筆記。

基本原則

TODO

原本的樣子

這裡有一個 Holiday 的 class,其中包含一個方法 checkValentinesDay,checkValentinesDay 會確定今天是否為情人節 (2/14),若是情人節就回傳字串「情人節快樂」,若不是情人節則回傳「今天不是情人節」。

export default class Holiday {
  checkValentinesDay() {
    const today = new Date();
    const month = today.getMonth() + 1;
    const day = today.getDate();
    return month === 2 && day === 14 ? '情人節快樂' : '今天不是情人節';
  }
}

簡單寫個測試。

describe('今天是情人節嗎?', function () {
  it('今天是情人節', () => {
    const holiday = new Holiday();
    const res = holiday.checkValentinesDay();
    expect(res).toBe('情人節快樂');
  });
});

今天是 3/29 的確不是情人節,一開測就 fail 了,這樣測不下去 XD 預期至少要能測試成功和失敗的兩種狀況,不然一年 365 天會有 364 天測失敗啊。

91 Unit Testing 單元測試實戰操練營 筆記

重構,拆解包山包海的函式

函式裡面包山包海,要怎麼專心測試特定的功能?

找出依賴,把它抽出來變成函式,也就是說,隔絕依賴,只測試單一功能。

這裡的依賴是取得今天日期的 new Date(),因此抽出來成為函式 getToday。這是由於在測試 checkValentinesDay 當中,取得日期是否正確與 checkValentinesDay 無關,若要確認是否正確取得日期可單獨寫測試來測它,也就是測試 getToday 即可。

export default class Holiday {
  getToday() {
    return new Date();
  }

  checkValentinesDay() {
    const today = this.getToday();
    const month = today.getMonth() + 1;
    const day = today.getDate();
    return month === 2 && day === 14 ? '情人節快樂' : '今天不是情人節';
  }
}

隔絕依賴,只測試單一功能

Mock 依賴的函式,就能專心測特定的函式。

既然取得今天日期的部份被獨立抽出來的,就不是測試 checkValentinesDay 的重點,重點擺在確認是否為情人節即可。這裡將 getToday mock 起來,我們就能依照測試需求回傳指定的值(如下,使用 mockReturnValue 指定回傳值),來測試預期的成功和失敗的兩種狀況。

describe('今天是情人節嗎?', function () {
  it('今天是情人節 2/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-02-14'));

    const res = holiday.checkValentinesDay();
    expect(res).toBe('情人節快樂');
  });

  it('今天不是情人節 2/12', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-02-12'));

    const res = holiday.checkValentinesDay();
    expect(res).toBe('今天不是情人節');
  });
});

91 Unit Testing 單元測試實戰操練營 筆記

重構測試

TODO

提出共用的判斷

經過一些需求變更,情人節必須包含白色情人節 (也就是 3/14)…

export default class Holiday {
  checkValentinesDay() {
    const today = this.getToday();
    const month = today.getMonth() + 1;
    const day = today.getDate();
    return (month === 2 || month === 3) && day === 14
      ? '情人節快樂'
      : '今天不是情人節';
  }
}

因此,在測試上就需要包含多種情況…

以下會測試 4 種狀況

describe('今天是情人節嗎?', function () {
  it('今天是情人節 2/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-02-14'));

    const res = holiday.checkValentinesDay();
    expect(res).toBe('情人節快樂');
  });

  it('今天是情人節 3/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-03-14'));

    const res = holiday.checkValentinesDay();
    expect(res).toBe('情人節快樂');
  });

  it('今天不是情人節 4/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-04-14'));

    const res = holiday.checkValentinesDay();
    expect(res).toBe('今天不是情人節');
  });

  it('今天不是情人節 2/12', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-02-12'));

    const res = holiday.checkValentinesDay();
    expect(res).toBe('今天不是情人節');
  });
});

抽出共用的判斷 shouldBeValentinesDay 與 shouldNotBeValentinesDay,這樣就能更清楚明瞭到底是判斷什麼,並且精簡程式碼。畢竟三個月後來看這段程式碼的人,大多會失憶…

describe('今天是情人節嗎?', function () {
  it('今天是情人節 2/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-02-14'));
    shouldBeValentinesDay(holiday.checkValentinesDay());
  });

  it('今天是情人節 3/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-03-14'));
    shouldBeValentinesDay(holiday.checkValentinesDay());
  });

  it('今天不是情人節 4/14', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-04-14'));
    shouldNotBeValentinesDay(holiday.checkValentinesDay());
  });

  it('今天不是情人節 2/12', () => {
    const holiday = new Holiday();
    holiday.getToday = jest.fn();
    holiday.getToday.mockReturnValue(new Date('2021-02-12'));
    shouldNotBeValentinesDay(holiday.checkValentinesDay());
  });
});

const shouldBeValentinesDay = (res) => {
  expect(res).toBe('情人節快樂');
};

const shouldNotBeValentinesDay = (res) => {
  expect(res).toBe('今天不是情人節');
};

91 Unit Testing 單元測試實戰操練營 筆記

隱藏無關的商業邏輯

測試要用來描述情境,無關的商業邏輯都要隱藏(例如:上例中情人節的年份)。

因此,修改如下,利用 givenToday 包裝要設定的日期,並放到環境假設的前置作業 beforeEach 中。

describe('今天是情人節嗎?', function () {
  let holiday = null;
  let mockGetToday = null;

  beforeEach(() => {
    holiday = new Holiday();
    mockGetToday = jest.fn();
    holiday.getToday = mockGetToday;
  });

  it('今天是情人節 2/14', () => {
    givenToday('2021-02-14');
    shouldBeValentinesDay(holiday.checkValentinesDay());
  });

  it('今天是情人節 3/14', () => {
    givenToday('2021-03-14');
    shouldBeValentinesDay(holiday.checkValentinesDay());
  });

  it('今天不是情人節 4/14', () => {
    givenToday('2021-04-14');
    shouldNotBeValentinesDay(holiday.checkValentinesDay());
  });

  it('今天不是情人節 2/12', () => {
    givenToday('2021-02-12');
    shouldNotBeValentinesDay(holiday.checkValentinesDay());
  });

  const givenToday = (today) => mockGetToday.mockReturnValue(new Date(today));

  const shouldBeValentinesDay = (res) => {
    expect(res).toBe('情人節快樂');
  };

  const shouldNotBeValentinesDay = (res) => {
    expect(res).toBe('今天不是情人節');
  };
});

看起來清楚明瞭許多 (๑•̀ㅂ•́)و✧

其他

什麼東西要寫測試?什麼不用寫測試?

到底要寫哪種測試?

洋蔥式架構或稱六角架構:核心 (domain / core business) -> adapter -> 外部應用程式

洋蔥式架構 六角架構

要怎麼寫測試呢?

(2024/02/10 更新)

The more your tests resemble the way your software is used, the more confidence they can give you. (你的測試越接近軟體的使用方式,它們就越能為你帶來信心。) by Kent C. Dodds

關於選擇怎樣的測試策略、安排多少比例在單元測試、整合測試或端對端測試,可參考 Pyramid or Crab? Find a testing strategy that fitsStatic vs Unit vs Integration vs E2E Testing for Frontend Apps

感想

非常感謝 91 老師來帶我們寫測試!

上課前覺得自己應該還滿會寫測試的 (不知哪來的自信?),結果第一個小範例就讓我見識到自己的無知和渺小 XD 像是怎麼拆解原始碼並隔離依賴來做測試的觀念就花了一些時間練習,還有過去「重構」只會發生在實作功能的原始碼上,從沒想過測試程式也要寫得模組化,都只能算是「有寫」而已。釐清觀念、吸收新知 (๑•̀ㅂ•́)و✧


(2021/04/29 更新) 公司內部分享,補上投影片

(2023/06/04 更新) 推薦閱讀好同事 Sean Chou 的筆記與心得 - 91 Unit Testing 單元測試實戰操練營 — 心得與學習筆記

(2024/01/16 更新) 關於 React 元件的單元測試該怎麼做?推薦閱讀好同事 Sean Chou 的大作 - 該怎麼用 Jest 測試你的 React 專案? Enzyme or Testing-library?


單元測試 自動化測試 Jest Unit Test front end testing 趨勢科技 Trend Micro sharing