91 Unit Testing 單元測試實戰操練營 筆記 - 寫測試的正確姿勢
29 Mar 2021參加 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 天測失敗啊。
重構,拆解包山包海的函式
函式裡面包山包海,要怎麼專心測試特定的功能?
找出依賴,把它抽出來變成函式,也就是說,隔絕依賴,只測試單一功能。
這裡的依賴是取得今天日期的 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('今天不是情人節');
});
});
重構測試
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 種狀況
- 2/14:是情人節
- 3/14:是情人節
- 4/14:不是情人節:因為判斷了月份和日期,測測看同月但不同日的狀況
- 2/12:不是情人節
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('今天不是情人節');
};
隱藏無關的商業邏輯
測試要用來描述情境,無關的商業邏輯都要隱藏(例如:上例中情人節的年份)。
因此,修改如下,利用 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('今天不是情人節');
};
});
看起來清楚明瞭許多 (๑•̀ㅂ•́)و✧
其他
什麼東西要寫測試?什麼不用寫測試?
- 一定要寫測試的是哪些東西呢?選擇投資回報率 (Return On Investment,ROI) 較高的,例如:錢、安全性、最常被修改但很容易改錯、過去有 bug 所補上的測試、主流程(主流程先寫、最後才寫防呆)、商譽。
- code coverage 怎麼用才是對的?從沒測到的比例,反推沒有測試 (uncovered) 的情境 -> 評估這些情境是否值得寫測試。
- 相對趨勢 > 絕對數字:每次進 code 都增加測試程式。
到底要寫哪種測試?
洋蔥式架構或稱六角架構:核心 (domain / core business) -> adapter -> 外部應用程式
- 核心:自家產品的主要服務
- adapter:串接自家產品與第三方服務的介面
- 外部應用程式:第三方服務
要怎麼寫測試呢?
- 核心:寫單元測試
- adapter -> 外部應用程式:寫整合測試
- 寫單元測試 + 整合測試,就有基礎建設、可以保持好的品質了
感想
非常感謝 91 老師來帶我們寫測試!
上課前覺得自己應該還滿會寫測試的 (不知哪來的自信?),結果第一個小範例就讓我見識到自己的無知和渺小 XD 像是怎麼拆解原始碼並隔離依賴來做測試的觀念就花了一些時間練習,還有過去「重構」只會發生在實作功能的原始碼上,從沒想過測試程式也要寫得模組化,都只能算是「有寫」而已。釐清觀念、吸收新知 (๑•̀ㅂ•́)و✧
(2021/04/29 更新) 公司內部分享,補上投影片。
–
(2023/06/04 更新)
推薦閱讀好同事 Sean Chou 的筆記與心得 - 91 Unit Testing 單元測試實戰操練營 — 心得與學習筆記