「反正測試都綠燈了,AI 順手把旁邊那個函式也改乾淨一點,應該沒差吧?」
這句話聽起來像是效率的展現——都已經打開這個檔案了,看到旁邊有一段程式碼寫得不太好,順手一起改掉,感覺是在做對的事。但「順手」這兩個字,恰好是 Refactor 階段最危險的入口。
Red-Green-Refactor 的第三步之所以能安全進行,前提是有一件事成立:你正在改動的每一行程式碼,都受到測試的保護。 這個前提在 AI 協作情境下,經常悄悄被打破,而且打破的方式很難被察覺——因為表面上看起來,測試還是全部綠燈。
Refactor 階段能安心進行,核心邏輯是:如果重構過程中不小心改壞了行為,測試會立刻告訴你。這句話成立的關鍵在於「你正在改動的這段程式碼」有沒有被測試涵蓋,而不是「這個檔案裡有沒有測試」。一個檔案可以有十幾個測試案例,但如果這次改動剛好動到其中沒有被任何測試打到的那幾行,測試套件全綠這件事完全不能證明什麼。
問題是,AI 在完成 Green 階段之後,很容易把「重構」的範圍自然而然地擴大。它被要求修好一個函式,過程中看到旁邊另一個函式的實作方式不夠一致、變數命名可以更清楚、有一段邏輯可以抽出來共用——這些觀察本身通常是對的,但「觀察到可以改善」跟「現在有安全網可以改」是兩件不同的事。這次任務的測試,是針對被要求修改的那個函式寫的,不代表旁邊那個函式也在同一張安全網底下。
如果順手改動的部分直接改壞了什麼,測試通常會抓到,這種情況反而不危險,因為它會立刻現形。真正危險的是另一種情況:順手改動的部分表面上沒有改變行為,只是換了一種寫法——例如把一段迴圈改寫成陣列方法鏈、把一個條件判斷的順序調整了、把某個中間變數省略掉直接內聯。這類改動如果原本就沒有測試涵蓋,測試自然也不會有任何反應,因為根本沒有東西在監看這段程式碼的行為有沒有變。
風險在於,這類「看起來只是換寫法」的改動,經常隱藏著不容易一眼看出的邊界情況差異——例如換了迴圈寫法之後,對空陣列或 null 的處理方式跟原本不一樣了。這種差異可能要到很久之後、某個邊界輸入真的出現時才會爆出來,而且到時候完全無法從「這次改動有沒有通過測試」回推出問題根源,因為當初根本沒有測試在觀察這段程式碼。
❌ 順手重構,範圍沒對齊測試覆蓋
任務:修正 calculateDiscount() 的門檻判斷邏輯(有測試涵蓋)
AI 的實際改動:
1. 修正 calculateDiscount() 的判斷邏輯 ← 有測試保護
2. 順手把旁邊的 formatCurrency() 從 for 迴圈改寫成 reduce ← 沒有測試涵蓋
3. 順手把 validateInput() 的參數順序調整成跟 calculateDiscount() 一致 ← 沒有測試涵蓋
→ 測試全綠不代表第 2、3 項改動沒有引入行為差異,
只代表沒有任何東西在檢查它們
✅ 重構範圍對齊測試覆蓋,其餘部分留到下一輪
任務:修正 calculateDiscount() 的門檻判斷邏輯
AI 的實際改動:
1. 修正 calculateDiscount() 的判斷邏輯 ← 有測試保護
回報:「注意到 formatCurrency()、validateInput() 也有可以改善的地方,
但目前沒有測試涵蓋,這次不動,如果要一併處理,
建議先補測試再重構。」
→ 改動範圍等於安全網範圍,額外的觀察誠實回報、
而不是直接動手
重構安不安全,不是看有沒有測試,是看你正在動的這幾行,有沒有被測試真正涵蓋到。
這個系列前面幾天談過的假陽性測試、硬編碼預期值,講的都是「測試本身有沒有問題」;今天談的是另一個維度——就算測試本身寫得沒問題,改動範圍跟測試覆蓋範圍對不齊,一樣會讓「測試綠燈」這件事失去意義。這兩個維度合起來,才是判斷一次改動安不安全的完整條件。
實務上比較穩妥的做法,是要求 AI 明確區分「這次任務要求的改動」跟「過程中觀察到但沒被要求的額外調整」,後者一律先回報、不直接動手,除非你確認過那部分也有測試覆蓋,或者你願意先花一輪補測試再處理。
回想你上一次讓 AI 重構一段程式碼:那次的改動範圍,跟原本被要求修改的部分完全一致嗎?如果 AI 順手多改了什麼,你有回頭確認過那部分有沒有測試涵蓋嗎?
明天要把這幾天談到的 Red、Green、Refactor 各自的風險收攏起來,看怎麼設計一套流程,逼 AI 老老實實走「先紅、後綠、再重構」的順序,而不是三步一次到位。