在 Day 15,我們反思了從 TDD Kata 中學到的寶貴經驗,正面回應了「沒時間寫測試」的迷思,並釐清了單元測試在測試金字塔中的核心地位。
我們已經深刻體會到 TDD 作為一種開發紀律。但是,我們不僅要知道一個工具 「如何使用」,更要知道它 「何時不該使用」,任何技術或方法論都有其適用的場景,將一套工具做為解決一切問題的「銀彈」是危險且不切實際的。
今天的目標:從 TDD 的熱情中抽離,以一種務實、客觀的態度,探討 TDD 的限制與權衡,學會做出明智的技術決策。
在深入探討之前,我們必須建立一個核心心態:
TDD 是一種強大的工具,但它終究是一個工具。
一個好的工匠會根據要打造的作品,來選擇最合適的鑿子;一個好的開發者也應該根據當前任務的情境,來選擇最合適的開發方式,盲目地在所有場景下都堅持「必須先寫測試」,並不是紀律性的體現,有時反而是僵化和低效的,真正的智慧在於權衡。
TDD 的核心循環「紅-綠-重構」有一個前提:你大致知道「綠燈」長什麼樣,也就是說開發者必須非常明確的知道需求,方能定義出一個清晰的、可驗證的預期結果。
但很多時候,尤其是在專案初期或研究新技術時,我們的目標是模糊的:
在這些探索性程式碼 (Exploratory Coding) 或 技術預研(也稱 Spike) 的場景中,我們的目標是「學習」和「發現」,而不是「打造一個健壯的產品」。 在這種情況下,強行 TDD 會像是在畫草圖前就開始精心雕琢畫框,不僅拖慢了探索的速度,也浪費了人力。這裡,快速寫出能工作的程式碼,甚至是在 REPL (Read-Eval-Print Loop) 環境中互動,是更高效的選擇。
TDD 極其擅長測試行為 (Behavior) 和邏輯 (Logic),我們可以輕易地驗證:
Add("1,2")
應該回傳 3。Generate(15)
應該回傳 "FizzBuzz"。但我們很難用 TDD 來測試外觀 (Appearance)。
這些主觀的、美學的、視覺的調整,TDD 無能為力。為一個按鈕的 CSS 顏色屬性寫單元測試,投入產出比極低。
但這並不意味著前端不能 TDD。前端的邏輯部分非常適合 TDD,例如:
這是最常見也最棘手的問題。你接手了一個有數千行程式碼的函式,它沒有任何測試,並且在函式內部直接讀取設定檔、連線資料庫、呼叫遠端服務……它像一團義大利麵,所有東西都纏繞在一起。
當你想為它新增一個小功能,並用 TDD 來開發。你嘗試寫下第一個測試,但立刻就遇到了巨大的阻礙:
在這種情況下,直接開始進行 TDD 式的「單元測試」是不可能的。這堵牆太厚了。
此時的策略不是放棄測試,而是改變測試的「層級」。 我們應該從更高層級的特性測試 (Characterization Tests) 入手 ,這種測試的目的不是驗證程式碼「是否正確」,而是「描述程式碼當前的行為」。
為這個巨大的函式編寫一個高層級的整合測試,讓它連同資料庫和檔案系統一起執行,捕捉並驗證它當前的輸出(哪怕這個輸出是錯的)。
現在,你有了一個「保護傘」。雖然傘很大、運行很慢,但它至少存在了。
在這個保護傘的保護下,你開始小心地進行重構,將資料庫的依賴、檔案的讀寫,從函式內部「抽離」出來,放到函式的參數中(依賴注入),一旦函式的核心邏輯與外部依賴解耦,你就為「單元測試」創造了條件。 現在,你可以為新的功能,重新開始你心愛的 TDD 循環了。
今天我們進行了一次客觀的探討,TDD 雖好,但絕非萬能,智慧地方在於懂得權衡與變通。
面對高度耦合的遺留程式碼,應先從高層級的特性測試入手,創造出可測試的「縫隙」後,再引入 TDD。
理解 TDD 的邊界,並不會削弱我們對它的信念,反而讓我們成為更成熟、更務實的軟體工匠,我們學會了為正確的問題,選擇正確的工具。
預告:第三階段正式開啟!Day 17 - 迎接 AI 隊友 - 設定 GitHub Copilot 的協作環境
在充分理解了 TDD 的威力與邊界之後,我們終於準備好迎接一位能幫助我們突破這些邊界、提升效率的夥伴了。明天,我們將正式讓 AI 加入我們的開發流程。