特徵測試 (Characterization Tests)

★★★★★Draft

FromWorking Effectively with Legacy Code

📚 From the Books

特徵測試 (Characterization Tests)Legacy Code

為遺留碼寫測試時,最怕的問題是「我根本不知道正確行為該是什麼」。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 的啟發法:

TIP

The Method Use Rule(方法使用規則): 在遺留系統裡用一個方法之前,先看它有沒有測試;沒有,就補一個。長期下來,測試會變成一種溝通媒介——別人一看就知道這方法能做什麼、不能做什麼。

🎯 針對性測試:對準你「要改的那條分支」

理解性測試寫完,還要回頭確認它真的蓋住了你要改的地方。假設你要把一段 if 抽成方法、搬到另一個類別:

啟發法很樸素:為要改的區塊寫測試,感覺一下它有沒有覆蓋改動會波及的部分;沒把握就加測試加到有把握;連加了都沒把握,就換一種更小步的改法。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading