測試失敗時,改程式還是改測試
測試失敗了,應該修改程式,還是修改測試?
這個判斷決定整個修復方向。判斷錯的產物是一個讓測試變綠、而需求被破壞的修改——它比紅燈更難發現,因為它看起來已經修好了。
核心原則:程式服務測試,測試服務需求
測試代表需求的具體描述。為了讓測試通過而修改測試本身,等於在需求上妥協。正確的做法是保持測試不變,調整程式實作直到符合期望。
唯一的例外:需求本身發生了變化——架構調整、業務流程重設計。這兩種情況的判斷,就是整個方法論的核心。
第一步:分類
面對測試失敗,先問「為什麼失敗」,不是「怎麼修」。
程式實作錯誤:需求沒變,但程式行為不符預期——錯誤輸出、邏輯判斷有誤、型別處理不當。處理方式直接:保持測試不變,修正程式。
最容易犯的錯誤,是在這種情況下改了測試的期望值,讓測試配合錯誤的程式。這等於讓錯誤行為成為「正確需求」。
架構變更需求:需求文件已更新,業務流程本質性改變,影響多個模組。這類情況確實需要調整測試,但前提是需求文件已反映這個變更。步驟:先確認文件、評估變更範圍、列出需修改的測試,最後才執行。跳過前置確認直接改測試,和無章法地亂改沒有區別。
觀測公開行為,不觀測內部實作
驗證的範圍限定在公開介面的輸入輸出與公開屬性的狀態變化,不碰私有方法的呼叫順序與私有屬性的中間值。
一旦測試觀測內部狀態,它就和具體實作綁定了,任何重構都會讓測試失敗,即使行為沒變。
1// 錯誤:觀測內部私有屬性
2test('書籍驗證應該檢查所有欄位', () {
3 final validator = BookValidator();
4 validator.validate(invalidBook);
5
6 expect(validator._titleValidated, isTrue); // 內部狀態
7 expect(validator._isbnValidated, isTrue); // 私有屬性
8});
9
10// 正確:觀測公開行為結果
11test('書籍驗證應該檢查所有欄位', () {
12 final validator = BookValidator();
13 final result = validator.validate(invalidBook);
14
15 expect(result.isValid, isFalse);
16 expect(result.errors, contains('標題不可為空'));
17 expect(result.errors, contains('ISBN 格式錯誤'));
18});三個常見的反模式
測試遷就程式:程式回傳錯誤值,而期望值被改成那個錯誤值。測試通過了,錯誤行為同時被登記成正確需求。正確做法是保持期望值不動、修正程式邏輯。
內部狀態依賴:測試直接存取 book._internalState 來驗證操作。一旦重構就失敗。改用公開方法 book.isAvailable() 驗證行為結果。
跳過文件檢查:判斷需求「應該」要改就直接改測試,而需求文件並未更新。正確做法是先查規格書、確認文件已反映變更、評估影響範圍,最後才動手。
驗收標準
修復前:錯誤類型已判斷、需求文件狀態已確認、受影響測試範圍已識別。
修復後:所有測試 100% 通過、無內部狀態曝露、無行為旁路、每個修改都有測試保護。
100% 通過率是底線:某個測試過不了時要找出的是失敗的原因,跳過與修改期望值都只是讓它從紅燈清單上消失。
測試該耦合行為而不是結構、兩派 TDD 對「單元」的定義差在哪,走 行為優先的 TDD。