iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Software Development

從 0 到 1:與 AI 協作的 Golang TDD 實戰系列 第 16

Day 16 - 何時不該用 TDD?誠實面對其限制與權衡

  • 分享至 

  • xImage
  •  

昨日回顧與今日目標

在 Day 15,我們反思了從 TDD Kata 中學到的寶貴經驗,正面回應了「沒時間寫測試」的迷思,並釐清了單元測試在測試金字塔中的核心地位。
我們已經深刻體會到 TDD 作為一種開發紀律。但是,我們不僅要知道一個工具 「如何使用」,更要知道它 「何時不該使用」,任何技術或方法論都有其適用的場景,將一套工具做為解決一切問題的「銀彈」是危險且不切實際的。

今天的目標:從 TDD 的熱情中抽離,以一種務實、客觀的態度,探討 TDD 的限制與權衡,學會做出明智的技術決策。

TDD 是一種工具,不是一種宗教

在深入探討之前,我們必須建立一個核心心態:

TDD 是一種強大的工具,但它終究是一個工具。

一個好的工匠會根據要打造的作品,來選擇最合適的鑿子;一個好的開發者也應該根據當前任務的情境,來選擇最合適的開發方式,盲目地在所有場景下都堅持「必須先寫測試」,並不是紀律性的體現,有時反而是僵化和低效的,真正的智慧在於權衡。

場景一:當你不知道要打造什麼時 - 探索性原型開發

TDD 的核心循環「紅-綠-重構」有一個前提:你大致知道「綠燈」長什麼樣,也就是說開發者必須非常明確的知道需求,方能定義出一個清晰的、可驗證的預期結果。

但很多時候,尤其是在專案初期或研究新技術時,我們的目標是模糊的:

  • 「我想試試看這個新的圖形 library 能不能畫出我想要的效果。」
  • 「我不確定這個第三方 API 回傳的資料結構是什麼,讓我請求一次看看。」
  • 「這個演算法的效能如何?讓我快速寫個腳本跑一下。」

在這些探索性程式碼 (Exploratory Coding)技術預研(也稱 Spike) 的場景中,我們的目標是「學習」和「發現」,而不是「打造一個健壯的產品」。 在這種情況下,強行 TDD 會像是在畫草圖前就開始精心雕琢畫框,不僅拖慢了探索的速度,也浪費了人力。這裡,快速寫出能工作的程式碼,甚至是在 REPL (Read-Eval-Print Loop) 環境中互動,是更高效的選擇。

場景二:當行為無法輕易斷言時 - UI 視覺調整

TDD 極其擅長測試行為 (Behavior) 和邏輯 (Logic),我們可以輕易地驗證:

  • Add("1,2") 應該回傳 3。
  • Generate(15) 應該回傳 "FizzBuzz"。

但我們很難用 TDD 來測試外觀 (Appearance)。

  • 這個按鈕的藍色是不是「恰到好處的藍」?
  • 這段文字的邊距調整 2 個像素後,看起來是不是「更和諧」?

這些主觀的、美學的、視覺的調整,TDD 無能為力。為一個按鈕的 CSS 顏色屬性寫單元測試,投入產出比極低。

但這並不意味著前端不能 TDD。前端的邏輯部分非常適合 TDD,例如:

  • 狀態管理邏輯: 當點擊「加入購物車」按鈕後,購物車的狀態 items 陣列長度是否加一?總金額是否正確更新?(例如在 Redux/Vuex/Pinia 的 store 中)
  • 元件的行為邏輯: 當傳入的 props.disabled 為 true 時,按鈕的 disabled 屬性是否也被設定為 true?
  • 對於視覺,我們應該使用更合適的工具,如 Storybook 進行元件隔離開發,或使用視覺化回歸測試 (Visual Regression Testing) 工具來捕捉非預期的 UI 變化。

場景三:當你面對一堵牆時 - 高度耦合的遺留程式碼

這是最常見也最棘手的問題。你接手了一個有數千行程式碼的函式,它沒有任何測試,並且在函式內部直接讀取設定檔、連線資料庫、呼叫遠端服務……它像一團義大利麵,所有東西都纏繞在一起。

當你想為它新增一個小功能,並用 TDD 來開發。你嘗試寫下第一個測試,但立刻就遇到了巨大的阻礙:

  • 你無法在不啟動資料庫的情況下建立這個函式的實例。
  • 你無法替換掉那個寫死的遠端服務呼叫。
  • 函式沒有回傳值,而是直接將結果寫入檔案,你無法輕易斷言。

在這種情況下,直接開始進行 TDD 式的「單元測試」是不可能的。這堵牆太厚了。
此時的策略不是放棄測試,而是改變測試的「層級」。 我們應該從更高層級的特性測試 (Characterization Tests) 入手 ,這種測試的目的不是驗證程式碼「是否正確」,而是「描述程式碼當前的行為」。

為這個巨大的函式編寫一個高層級的整合測試,讓它連同資料庫和檔案系統一起執行,捕捉並驗證它當前的輸出(哪怕這個輸出是錯的)。

現在,你有了一個「保護傘」。雖然傘很大、運行很慢,但它至少存在了。
在這個保護傘的保護下,你開始小心地進行重構,將資料庫的依賴、檔案的讀寫,從函式內部「抽離」出來,放到函式的參數中(依賴注入),一旦函式的核心邏輯與外部依賴解耦,你就為「單元測試」創造了條件。 現在,你可以為新的功能,重新開始你心愛的 TDD 循環了。

今日總結

今天我們進行了一次客觀的探討,TDD 雖好,但絕非萬能,智慧地方在於懂得權衡與變通。

  • TDD 不適用於目標模糊的探索性原型。
  • TDD 專注於行為和邏輯,而非主觀的視覺外觀。

面對高度耦合的遺留程式碼,應先從高層級的特性測試入手,創造出可測試的「縫隙」後,再引入 TDD。
理解 TDD 的邊界,並不會削弱我們對它的信念,反而讓我們成為更成熟、更務實的軟體工匠,我們學會了為正確的問題,選擇正確的工具。

預告:第三階段正式開啟!Day 17 - 迎接 AI 隊友 - 設定 GitHub Copilot 的協作環境
在充分理解了 TDD 的威力與邊界之後,我們終於準備好迎接一位能幫助我們突破這些邊界、提升效率的夥伴了。明天,我們將正式讓 AI 加入我們的開發流程。


上一篇
Day 15 - TDD 實戰回顧與核心問答
下一篇
Day 17 - 迎接 AI 隊友:設定 GitHub Copilot 的協作環境
系列文
從 0 到 1:與 AI 協作的 Golang TDD 實戰30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言