不只是 TDD #2, 兩個版本自我驗證 + 執行期驗證
摘要提示
- 目標提升: 解題不只求正確,還要涵蓋效能、可維護性與可靠性
- 擴展TDD: 單元測試從開發期延伸到執行期,讓程式在Runtime也能自我保護
- 可敬的對手: 先實作一個保守但可靠的版本,作為新演算法的比對基準
- 自動產生測試: 以隨機或全列舉方式大量生成測試案例增加涵蓋率
- 兩版本驗證: 新舊版本對跑,結果一致視為正確,降低改寫風險
- 驗算思維: 借鏡數學「驗算」,用不同作法交叉核對結果
- Fail Fast: 在程式內部插入ASSERT,盡早在執行期發現不一致與異常
- 條件式編譯: 用自定義符號控制除錯輔助碼,避免影響提交與效能
- 案例示範: Shortest Palindrome的保守版與隨機測試;ZumaGame的執行期檢查
- 架構實務: 新舊版本對照亦適用於微服務重構,確保對外行為不變
全文重點
文章延續前篇TDD主題,主張「TDD不只是寫單元測試」。在解LeetCode或做實務開發時,正確性、效能與可維護性同等重要。作者提出兩個實務方法:其一,把單元測試觀念延伸到執行期,於主程式中加入維護(ASSERT)與診斷碼,實踐Fail Fast;其二,建立「可敬的對手」,先寫出一個保守且容易實作、但結果可靠的版本,做為後續優化版的自動驗證器,並配合自動產生大量測試資料,讓機器擴大涵蓋率。
以Shortest Palindrome為例,先做一個O(n^2)思路的保守版:從尾往前找與首字相同的位置,檢查前綴是否為回文,若是即將後綴反轉補到前方,得到最短回文。此版本不追求最佳時間複雜度,但易於正確,能成為對照標準。接著開發新演算法時,透過單元測試框架同時呼叫新舊兩版,對比輸出一致性;並用程式隨機產生測試字串(指定長度與字元集),或在有限範圍內全列舉,實現大量自動化測試,讓研發能集中在優化與正確性上,同步降低改寫風險。
文章更進一步把測試延伸至執行期,以ZumaGame為例在主程式中插入ASSERT與除錯輔助:覆寫ToString便於Debugger觀察狀態;維持統計資料與實際盤面的一致性檢查,於關鍵操作前後呼叫斷言方法,一旦不一致即丟出例外,以最小成本定位是演算法錯誤或程式Bug。為避免影響提交與效能,這些輔助碼透過條件式編譯與自定義符號控制,只在本機除錯時啟用。
作者總結,工具與技術會變,但背後的知識與觀念(例如驗算、Fail Fast、以可靠基準做比對)長期有效。兩版本自我驗證不只適用演算法題,在大型系統由單體拆分到微服務的重構過程,也能在Runtime佈建新舊路徑的結果比對,確保對外行為不變,為穩健重構護航。打好這些基礎,比單純追逐語言或框架更能構成軟體工程的核心競爭力。
段落重點
前言:跳出狹義TDD,瞄準更高目標
作者指出,若只想「解題」看前篇即可;但若面向工作與專案,就需要兼顧正確性、效率、可維護性。TDD不只是單元測試,還要預備面對不可預期情況。提出兩條路徑:把測試延伸至執行期、用機器自動擴展測試案例。靈感來自AlphaGo自我對奕:創造「可敬的對手」,讓它大量產出案例並告訴我們是否正確。延續前一篇題目Shortest Palindrome,示範如何落實。
第二步:先寫保守版本,打造可靠基準
對不熟悉的新題,先寫保守但容易正確的版本,作為日後優化的對照。以Shortest Palindrome為例,實作流程為:由尾向前尋找與首字相等的位置,檢查該位置前綴是否為回文;若成立,將後綴反轉補到前方得到最短回文。此法不追求最佳時間複雜度,但易於驗證正確性。這個「可恥卻有用」的版本,是新演算法的驗證器,能在之後大量測試中供比對,確立功能等價基準。
第三步:兩版本對照與自動化測試
已知題庫測資很少且效能未知,改寫風險高。科學作法是:新舊版本對跑,以保守版結果作為真值。測試資料來源分「靜態」與「動態」:靜態由人工補入極端案例;動態則程式隨機產生大量字串,或在可控範圍做全列舉。單元測試中針對每個隨機字串同時呼叫新舊兩版,比對輸出一致性。若新版本複雜,可先離線用保守版批量產生靜態測資。這樣研發能專注最佳化,同時以機器擴大覆蓋,降低錯誤外洩。
第四步:在主程式插ASSERT,把測試延伸到Runtime
單元測試屬黑箱且多停留在開發/建置期,許多狀況需在執行期監控。做法是:在主程式穿插ASSERT與診斷碼,落實Fail Fast。以ZumaGame為例:覆寫ToString配合Debugger快速檢視狀態;用條件式編譯與自定義符號控制這些輔助碼僅在本機啟用;維護統計資料與實際盤面的同步,於關鍵操作前後呼叫Assert方法,不一致即丟例外。好處是能即時區分演算法與Bug並縮小排查範圍,代價則是需事先規劃與埋點。
結論:觀念長青,方法可複用到重構與微服務
文章強調基礎觀念的重要性:驗算思維、Fail Fast、以可靠基準做等價驗證等,歷久彌新。兩版本自我驗證不僅適用演算法練習,在從單體走向微服務的重構中,也可於Runtime佈建新舊流程的結果比對,確保對外行為不變,讓重構更安心。工具會變,但這些知識與方法通用且可複製;只要基礎紮實,面對新技術能迅速上手並正確落地。作者以此鼓勵工程師精進內功,並祝新年快樂。
資訊整理
知識架構圖
- 前置知識:學習本主題前需要掌握什麼?
- 基本單元測試觀念與框架使用(如 MSTest、xUnit、NUnit)
- 演算法與時間複雜度基礎(如 O(n^2), O(n log n))
- 偵錯與除錯工具使用(Debugger、日誌、堆疊檢視)
- 程式語言的條件式編譯與巨集(如 C# 的 #if、#define、Conditional Compilation)
- 基本資料結構與字串處理
- 核心概念:本文的 3-5 個核心概念及其關係
- 兩版本自我驗證:先實作「保守但正確」版本,再以其作為標準比對新版本結果,形成自動驗證閉環。
- 動態擴充測試案例:以隨機或窮舉生成器大量產生輸入,擴大涵蓋率,降低遺漏邊界情況。
- 執行期驗證(Fail-Fast + Assert):在程式關鍵點插入斷言與一致性檢查,於 runtime 及早發現錯誤。
- 測試分層與職責分離:靜態測試(人工挑選案例)+動態測試(自動生成)+內嵌斷言(執行期),各司其職形成防護網。
- 條件式編譯控制維護碼:以編譯旗標控制 Debug-only 的 ToString/Assert/統計檢查,兼顧效能與維護性。
- 技術依賴:相關技術之間的依賴關係
- 新演算法實作 依賴 保守版本作為正確性基準
- 動態測試資料生成器 依賴 隨機分佈策略與輸入約束(長度、字元集)
- 單元測試框架 依賴 兩版本實作的可重複呼叫接口(API 一致性)
- 執行期 Assert/監控 依賴 條件式編譯旗標與除錯環境
- 效能最佳化 依賴 可靠的正確性護欄(兩版本比對與執行期檢查)
- 應用場景:適用於哪些實際場景?
- 演算法題解與競賽:先求對再求快,以保守版護航最佳化。
- 大型重構與微服務切分:新舊系統在 runtime 比對結果,降低行為改變風險。
- 高風險改動(核心模組、關鍵演算法):在關鍵路徑加入斷言與一致性檢查。
- 測試資源有限時:以自動生成測試案例擴充涵蓋率,提高缺陷攔截率。
- 線上問題定位:以 Fail-Fast 及早暴露不變條件被破壞之處,縮小故障定位範圍。
學習路徑建議
- 入門者路徑:零基礎如何開始?
- 了解單元測試基礎、斷言語法與測試生命周期(TestInitialize/Teardown)
- 從簡單演算法題著手,先寫出保守版(如 O(n^2))並通過少量手動案例
- 練習以保守版當 oracle,寫新版本並以 Assert.AreEqual 比對兩版輸出
- 練習撰寫隨機輸入生成器(限制長度與字元集),擴大測試數據
- 進階者路徑:已有基礎如何深化?
- 建立靜態+動態測試策略:人工挑選邊界案例+隨機大量測試
- 為核心資料結構加入 ToString 與一致性檢查(狀態-統計比對)
- 學會條件式編譯與本地偵錯旗標(如 #define LOCAL_DEBUG)
- 在新演算法中逐步最佳化,保持兩版一致性紅線,確保不破壞行為
- 實戰路徑:如何應用到實際專案?
- 在重構或服務切分時,於關鍵輸入輸出點埋入新舊結果比對(灰度或暗流量)
- 將動態測試生成器產出資料固化為回歸測試集,納入 CI
- 在核心路徑加入可控的 runtime Assert/監控,並提供開關(環境變數/編譯旗標)
- 對性能關鍵模組建立「保守基準+最佳化實作」雙實作管線,持續以測試守護
關鍵要點清單
- 兩版本自我驗證:以保守版作為正確性 oracle,比對新版本輸出確保一致 (優先級: 高)
- 保守版先行:先實作易於正確的基準解,為後續最佳化提供護欄 (優先級: 高)
- 動態測試資料生成:以隨機/窮舉生成輸入,顯著提升涵蓋率 (優先級: 高)
- 靜態測試案例策劃:人工挑選極端與邊界案例(超長、同質、特殊字元) (優先級: 高)
- 執行期斷言(Fail-Fast):在關鍵不變條件上加入 assert,及早暴露錯誤 (優先級: 高)
- 條件式編譯控制:用 #if/#define 分離維護碼與提交碼,兼顧效能 (優先級: 中)
- 可讀性 ToString:覆寫 ToString 以便除錯觀察複雜物件狀態 (優先級: 中)
- 一致性檢查函式:設計 AssertStatisticData 等檢查,驗證狀態與統計一致 (優先級: 高)
- 測試驅動最佳化:在既有測試護航下更安全地更換演算法與資料結構 (優先級: 高)
- 測試數量與成本平衡:大量動態測試可在開發期跑,提交時關閉 (優先級: 中)
- API 穩定性:新舊版本保留相同介面,便於自動化對比 (優先級: 中)
- 產測環境分離:只在本地或 Debug 模式開啟維護碼,避免影響線上效能 (優先級: 高)
- 重構與微服務遷移:以新舊結果比對確保行為不變,降低風險 (優先級: 高)
- 邊界條件設計:明確定義輸入限制(長度、字元集)以設計生成器 (優先級: 中)
- 測試資產沉澱:將隨機找到的缺陷輸入固化為回歸測試集 (優先級: 中)