為遺留碼寫測試時,最怕的問題是「我根本不知道正確行為該是什麼」。Feathers 的答案是:先別管「應該」怎樣——記錄它「實際」怎樣。特徵測試不是用來找 bug 的黃金標準,而是一張安全網:把系統當前的真實行為釘住,日後任何改動一旦動到它,就會以「差異」的形式跳出來。
🧠 為什麼是這種測試
一般講測試,指的是「用來找 bug」。但在遺留碼裡,這不是首要目標——保留既有行為才是。
IMPORTANT
特徵測試(characterization test)記錄的是程式碼的實際行為,不是它「應該」有的行為。它的價值不在直接抓 bug,而在:當你改動時,讓 bug 以「與當前行為的差異」形式顯現出來。
這聽起來像作弊——把程式的輸出貼回測試,這也算測試?換個角度:這些不是「軟體必須符合的規格」,而是一個機制,讓未來的意外變成紅燈。它靜靜地記錄真相,等你動它的那天出來說話。
⚖️ 撰寫節奏:讓失敗訊息告訴你真相
特徵測試的寫法跟 TDD 相反——TDD 先寫「期望」再讓碼滿足它;特徵測試先讓碼跑,再把它實際吐出的東西寫成期望。
步驟:寫一個你明知會失敗的斷言,讓紅字報出實際值
// ① 故意寫一個一定會失敗的斷言(我還不知道真實輸出)
test("PageGenerator 的輸出", () => {
const g = new PageGenerator();
expect(g.generate()).toBe("fred"); // 亂猜的期望
});
// ▶ 失敗:expected "fred" but was ""
// ── 失敗訊息剛剛告訴了我:實際回傳是空字串// ② 把期望改成「實際行為」——測試變綠,一條事實被釘住
test("新建的 PageGenerator 產生空字串", () => {
const g = new PageGenerator();
expect(g.generate()).toBe("");
});// ③ 餵更複雜的輸入,重複同一套路:先失敗、讀出真相、釘住
test("餵入 base row 後的輸出", () => {
const g = new PageGenerator();
g.assoc(RowMappings.getRow(Page.BASE_ROW));
// 失敗訊息報出實際值後,把它貼進來:
expect(g.generate()).toBe("<node><carry>1.1 vectrai</carry></node>");
});五步循環:在測試框架裡跑它 → 寫一個會失敗的斷言 → 讓紅字說出實際行為 → 改斷言去期望實際值 → 重複。
WARNING
釘住的過程中撞到看起來像 bug 的行為怎麼辦? 別直接把它當「正確」寫進測試套件。標記為可疑、去調查:若系統從未上線,直接修;若已上線,得先想「有沒有人已經依賴這個行為」。特徵測試的職責是記錄現況,不是替現況背書。
🔎 從哪裡下手釘一個類別
要 characterize 一個類別、卻不知從何測起時,Feathers 的啟發法:
- 先抓高層意圖:這類別大概在幹嘛?為「最簡單能想到的行為」先寫一個測試。
- 找糾結的邏輯塊:看不懂的分支,引入一個 sensing variable 確認你真的執行到那段。
- 想極端值與 invariant:極端輸入會怎樣?有沒有在物件生命週期中始終為真的條件?寫測試釘住它們。
- 讓好奇心帶路:發現一個職責,就列出「它可能怎麼出錯」,試著寫測試觸發。
TIP
The Method Use Rule(方法使用規則): 在遺留系統裡用一個方法之前,先看它有沒有測試;沒有,就補一個。長期下來,測試會變成一種溝通媒介——別人一看就知道這方法能做什麼、不能做什麼。
🎯 針對性測試:對準你「要改的那條分支」
理解性測試寫完,還要回頭確認它真的蓋住了你要改的地方。假設你要把一段 if 抽成方法、搬到另一個類別:
- 不會被改到的分支——有測試更好,但非必要。
- 會被改到的分支(例如某個算式從內嵌變成參數傳入)——一定要有測試。
- 記得連「條件為 false」的路徑也確認到。
啟發法很樸素:為要改的區塊寫測試,感覺一下它有沒有覆蓋改動會波及的部分;沒把握就加測試加到有把握;連加了都沒把握,就換一種更小步的改法。
🔑 Takeaways
- 特徵測試記錄的是實際行為,不是「應該的行為」;它是安全網,不是規格。
- 節奏:寫會失敗的斷言 → 讓紅字報出真實值 → 把真實值釘成期望(與 TDD 反向)。
- 撞到疑似 bug 的行為:標記、調查,別直接當正確寫進去。
- 用高層意圖、糾結邏輯、極端值、invariant 當切入點;遵守「用方法前先確認它有測試」。
- 寫完做針對性檢查:確認測試真的覆蓋你要改的那條分支——這正是安全 重構 的前提。
No notes yet — jot your takeaways or Q&A here.