第 4 / 9 部 待錄

測試設計

《Clean Craftsmanship》Podcast 準備稿:測試設計.打破一對一對應與轉換優先前提

書名: 整潔的軟體工藝 Clean Craftsmanship(Disciplines, Standards, and Ethics) 作者: Robert C. Martin(Uncle Bob) 系列: Reading Uncle Bob(恩普拉氏) 公開標題建議: Reading Uncle Bob|Clean Craftsmanship.為什麼你的測試一改就全壞 (4/9) 涵蓋範圍: Part I 第4章 Test Design(測資料庫/GUI、測試模式、脆弱測試、打破一對一對應、特定性 vs 通用性、轉換優先前提)

背景速覽

很多人學 TDD,前三個月就放棄了——不是因為不會寫測試,而是因為改一行產品碼,二十個測試一起爆。這集處理的就是這個勸退魔王:脆弱測試(Fragile Tests)。Uncle Bob 的診斷很犀利:脆弱性永遠是設計問題,而且測試本身就需要被設計。這章從怎麼測資料庫/GUI(提示:別測它們)講起,帶出脆弱測試的根因「一對一對應」,最後丟出全書最理論的一塊——轉換優先前提(TPP)。

一句話重點

脆弱測試的元凶是「為每個產品模組 X 都配一個 XTest」這種一對一對應——它把測試的結構釘死在產品碼的結構上,所以你一動產品碼、測試就跟著垮;解法是用介面層把測試和實作細節隔開,讓「修改自由」回到你手上。

值得討論的重點

1. 別測資料庫,別測 GUI——把它們縮到最小

Uncle Bob 對外部依賴的策略很一致:不要測它,把它隔離成薄薄一層

  • 資料庫:兩條規則——不要測資料庫本身(假設它是好的)、把資料庫和業務規則解耦。手段是 Gateway 介面:每種查詢一個方法(getEmployeesHiredAfter(2001)),SQL/ORM 全藏在 GatewayImpl 裡,業務規則測試時用 stub/spy 換掉它。
  • GUI:三條規則更俏皮——不要測 GUI、測 GUI 以外的所有東西、GUI 比你想像的更小。把格式化、按鈕狀態、數字格式全抽到 Presenter,產出一個叫 View Model 的純資料結構,GUI 只負責照著畫。他甚至不建議用 GUI 自動化測試工具(慢又脆,GUI 是最易變的模組)。

這段的精神一句話:凡是難測的,都把它推到系統邊緣、縮到小到不值得測。

2. 一對一對應的陷阱:脆弱測試的真正病根

這是本章的核心診斷。直覺上,我們會為 CustomerCustomerTest、為 RentalRentalTest——每個產品類別配一個測試類別。Uncle Bob 說這正是毒藥:它在產品碼和測試之間造出結構耦合。更糟的是傳遞性——βTest 不只耦合 β,還會因為 β 用到 γδ 而間接耦合它們,於是「對 δ 的微小改動」可能同時弄壞 βTestγTestδTest。Rule 12:把測試的結構與產品碼的結構解耦。 解法是把測試打在「模組族群的介面」上——αTest 只認 α 這個 facade,背後的 α1~α5 對它透明。書裡的 Video Store 範例完整演了一遍:最後 RentalCalculatorTest 根本不知道 RentalMovieRegularMovie 的存在,它們全是被間接測試的。

3. 轉換優先前提(TPP):用測試「逼出」演算法的理論

這是全書最學院派、也最迷人的一段。在紅綠重構裡,Green 那步其實是一連串轉換(transformation)——對現有碼做最小修改、讓它行為更通用。Uncle Bob 列出一張優先順序表(從最具體到最通用):

{} → Nil → Constant → Variable → Selection
→ List → Iteration → Recursion → Mutated Value

搭配 Rule 13「測試越具體,程式碼越通用」——測試和產品碼往相反方向演化:測試長成一串線性的約束規格,產品碼長成一個豐富的邏輯族群。核心心法:當有好幾種轉換都能讓測試過時,選表上位置較高(較簡單)那個,往往導向更好的解。Fibonacci 範例是反面教材:選了 Statement → Recursion,得到優雅但 fib(40) 要跑 9 秒的指數爆炸;換成 Selection → Iterationfib(100) 只要 10 毫秒(Rule 14:轉換導致次優解時,換一個轉換)。

注意事項

⚠️ 「一對一對應」這條最容易被誤解。 Uncle Bob 不是說「永遠不要 XTest 配 X」——他在後面(重構章與本章)也承認:在模組/元件層級,測試映射產品結構是好的(方便快速跑相關子集)。要打破的是「模組以下、類別層級的細節對應」。講的時候要把這個邊界劃清楚,否則聽眾會走極端、把所有單元測試都拆掉,反而更糟。

⚠️ TPP 是「前提(Premise)」不是「定理」——Uncle Bob 自己強調這只是個有用的直覺啟發,沒有數學證明。它在「演算法/純函式」題材上很漂亮,但對「協調一堆物件、處理副作用」的應用程式碼,TPP 的指導力會弱很多。別把它講成放諸四海皆準的公式。

⚠️ 「不要用 GUI 自動化測試工具」這條,在 2021 年的脈絡下偏保守。現代的 Playwright/Cypress 比當年的 Selenium 穩定太多,端到端測試在很多團隊是必要的關卡。Uncle Bob 的「GUI 慢又脆」批評有道理,但結論要打折——更穩妥的說法是「把業務規則的測試從 UI 抽出來,UI 測試只留少量、守關鍵流程」,這跟測試金字塔一致。

專家補充

💡 「脆弱性永遠是設計問題」是這章最值得反覆咀嚼的一句。它把「測試很煩」這個情緒,重新導向一個建設性問題:如果我改一處就壞一片,那不是測試的錯,是我的模組邊界劃錯了。 測試在這裡扮演的是「設計品質的金絲雀」——它替你提早聞到耦合過緊的瓦斯味。這個視角和上一集「不確定性原理」、和簡單設計章的「可測 = 解耦」是同一條主軸。

💡 Video Store 是軟體界的「Hello World 級重構範例」——它最早出自 Martin Fowler《Refactoring》第一章。Uncle Bob 在這裡用它示範「測試如何不耦合到內部類別」,可以跟聽眾說:這個例子你會在好幾本經典看到,值得自己動手寫一遍。

💡 TPP 的深層意義其實是:遵循這個優先順序會自然把你推向函數式風格的解(先常數、先不變數、用迭代/遞迴而非可變狀態,可變狀態排最後)。這跟 Reading Uncle Bob 系列最後一本《Functional Design》遙相呼應——Uncle Bob 晚年越來越靠向 FP,TPP 是個早期的伏筆。

討論問題

🎙️ 開場鉤子:「你有沒有改過一行 code,結果二十個測試一起變紅?那不是你倒楣,是你的測試被設計壞了——今天就來修這個。」

🎙️ 自問自答:「為什麼大家學 TDD 三個月就放棄?」——脆弱測試。而脆弱永遠是設計問題,不是測試太多的問題。

🎙️ 拋給聽眾:「你的測試是打在『類別』上,還是打在『模組族群的介面』上?前者每次重構都受傷,後者讓你能自由改內部。」

🎙️ 帶走的一題:「找一個你最近改了會連鎖弄壞一堆測試的類別——想想:是不是該在它和測試之間插一道介面(facade),把細節藏起來?」

更大範圍關聯

  • 脆弱測試的延伸閱讀:Vladimir Khorikov《Unit Testing Principles, Practices, and Patterns》(2020) 把「脆弱測試 vs 抗重構」這套講得比本章更系統,是絕佳的進階配對。
  • Video Store 的源頭:Martin Fowler《Refactoring》第一章——同一個例子,一個從重構角度、一個從測試設計角度,對照著讀很有收穫。
  • TPP 的脈絡:轉換優先前提原是 Uncle Bob 2010 年部落格文章,本章是它的書面定稿;它和 Kent Beck《TDD by Example》的「三角測量(triangulation)」是同一個問題的不同切法。
  • 領域定位:用 [軟體工程領域切分藍圖] 看,這章站在「測試」與「架構/設計」的交界——它最大的主張是「測試的設計品質,等於、甚至先於產品碼的設計品質」。

錄製建議

  • 建議時長:約 22-25 分鐘。配比:測資料庫/GUI 5 分、脆弱測試與一對一對應 8 分、特定性 vs 通用性 3 分、TPP 6 分。
  • 討論策略:用「改一行壞二十個」的痛點開場,命中率極高。一對一對應是這集主菜,務必把「該打破的是類別層級、不是模組層級」這條邊界講清楚,免得聽眾走極端。TPP 偏理論,建議只講「優先順序表的精神」和「Fibonacci 那個 9 秒 vs 10 毫秒」的反差,不要陷進每一條轉換的細節——Podcast 講不動那個。結尾把這集接回上一集的「測試是設計的金絲雀」,整個 Part I 的測試三章(EP2-4)就收成一條完整的線。