iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Software Development

AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護系列 第 12

Day 12|AI 寫完再測,和先測再改有什麼差別?比較直接完成、TDD 與 TCR

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天比較類別拆分時,控制組、行數上限與內聚拆分都能把升級通知做對,也都通過最後的行為驗證。

若只看終點,三種結構都像是成功的候選。可是最後一片綠燈,只能證明它們抵達了終點,沒有說明途中累積了多少尚未驗證的修改,也沒有告訴我,走偏時得退回多遠。

Agent 可能一次修改兩百多行,完成 Production Code 後才補 Tests;也可能先取得有效 Red,再開始實作。兩種做法最後都能顯示綠燈,開發過程留下的證據與還原範圍卻完全不同。

九次實驗跑完後,Direct 三次都把功能做對;TDD 三次都先取得有效 Red,再完成 Green;TCR 則有一次依規則回復兩個失敗批次,最後停在尚未完成整項任務的 Green 狀態。這項 24 小時邊界需求,我最後採用 TDD Run 02。

原因不是 TDD 這個名稱比較正統,而是它先讓舊 Production 對新需求變紅,又用同一份時間快照完成邊界判斷;相較之下,Direct 缺少中途證據,本次 TCR 的最後一批也仍然太大。

今天要比較的,就是測試介入時間如何影響修改範圍、過程證據與還原成本。

〈測試紀律〉用 TDD、TCR 與小批次控制修改節奏

《無瑕的程式碼 第二版》的〈測試紀律〉把測試放進整個開發週期。功能寫完後的驗收,只是其中一個階段。

第一項紀律是 TDD,全名為 Test-Driven Development,也就是測試驅動開發。它可以整理成三個動作:

  1. 舊系統還沒有因為缺少目標行為而出現失敗測試前,先不要修改 Production Code。
  2. 測試只寫到足以表達目前缺少的行為,並且因正確理由失敗。
  3. Production Code 只補到足以讓測試通過,接著才整理命名、重複與結構。

這就是常聽到的 Red、Green、Refactor:Red 先顯示需求缺口,Green 用最少修改補上行為,Refactor 則在行為受到保護後整理 Code。

第二項紀律是 TCR,全名為 Test && Commit || Revert。每一小步都必須維持測試通過;通過就建立 Commit,失敗就放棄這一小批修改,回到上一個可以工作的狀態。

第三項紀律是 Small Bundles,繁體中文版譯為「小批次封裝」。它要求每次只處理容易理解、驗證與還原的一小批修改。Commit 數量只能當成線索;還要確認每一批承擔幾項行為,以及失敗時必須放棄多少內容。

這三項紀律共同控制的是回饋速度與錯誤半徑。測試愈早提供回饋,開發者就愈早知道目前的理解能不能承接下一步,也比較不必在一大批修改裡慢慢找錯。

不過,測試資料、需求理解或驗收條件一旦寫錯,再嚴格的紀律也可能把開發帶往錯誤方向。我把書末〈Clean Code 大辯論〉中的不同立場讀成這項提醒:TDD 仍要接受專案情境與實際成本的檢驗。今天不是替三種節奏排永久名次,而是看它們分別控制了什麼。

保留測試品質,不必強迫 Agent 模仿人類的微型 TDD 節奏

Uncle Bob 在近期訪談裡也重新調整了自己操作 Agent 的方式。他仍然重視 TDD,卻不再要求 Agent 完整模仿人類「先寫一小段 Test,再補一小段 Production Code」的微型節奏;他可以接受 Agent 先完成一個函式,再替這個函式補上測試。

調整的是工作粒度,測試品質沒有被拿掉。他的流程仍保留 Unit Test、Gherkin、Coverage、Mutation Testing 與 QA 等多層驗證。

我在意的也是這個區別:TDD 的價值包含提早說清楚答案、縮短回饋與限制錯誤範圍;人類為了維持專注形成的每個操作細節,未必都要原封不動套在 Agent 身上。

所以今天不會用「有沒有完整模仿 TDD 儀式」替候選評分。我會檢查 Red 是否真的指出需求缺口、Green 有沒有削弱測試、失敗時能退回多遠,以及最後的獨立 Oracle 能不能抓住錯誤。價值要守住,節奏則用實驗重新校準。

直接完成、TDD 與 TCR 的差別在測試何時介入

三組都會寫測試,也都要通過相同的最終驗證。差異集中在 Agent 何時看見測試結果,以及失敗後哪些修改還能留下:

開發節奏 Tests 何時介入 測試通過後 測試失敗後 本篇要觀察什麼
直接完成(Direct Completion) Production Code 與 Tests 可以在同一階段完成,最後才整批驗證 全部內容建立一個 Commit 整批重新判斷或放棄 一次完成能不能做對,以及還原範圍有多大
TDD Production Code 修改前先取得有效 Red 補最小 Green,再進入 Refactor 保留 Red,依失敗原因補足行為 測試是否先看見需求缺口
TCR 每一小批修改後立刻測試,全程維持 Green 立即 Commit 捨棄這一批,回到上一個 Green 狀態 批次是否真的夠小,以及失敗能不能安全回復

本文把「測試全部通過、可以獨立保留,也能安全回復的 Git 狀態」稱為 Green Checkpoint。這是本次實驗用來比較還原能力的名稱。

TCR 名稱裡的 revert,指的是「測試失敗後回到上一個 Green 狀態」,不限定必須執行哪一條 Git 指令。

這次失敗的修改尚未 Commit,因此我使用 git restore <明確檔案>,把指定檔案還原到目前的 HEAD。它不會建立新 Commit;git revert 則會用一個新的反向 Commit 抵銷先前修改,兩者用途不同。

本篇採用嚴格維持 Green 的 TCR 版本。TCR 還有其他實作變體,今天的結果只適用於這份 Prompt 與操作規則。

直接完成、TDD 與 TCR 在測試時機、回饋與回退半徑上的差異

圖:三種節奏的差異,在於紅燈何時出現、修改何時提交,以及失敗時要放棄多少內容。

為什麼用 24 小時升級通知測試三種節奏?

實驗從昨天接受的內聚版本開始。Work Item API 原本會找出到期且未完成的項目,標記為 Overdue,再傳送一般逾期通知。今天新增一條規則:

Priority == "High" 且至少逾期 24 小時時,改走 SendEscalationAsync;其他項目維持 SendOverdueAsync

表面上只多一個條件,實際上同時帶來六種測試壓力:

壓力 要守住的行為
精確邊界 剛好 24 小時要升級,少一個 Tick 仍走一般通知
分流 High 與非 High 必須走不同通知路徑
重跑 已是 Overdue 的項目仍要依相同規則再次通知
失敗 通知回傳 false 時仍要儲存 Overdue
副作用順序 Exception、Cancellation 與通知先於 Save 的順序不能漂移
外部契約 HTTP Route、Status Code、Response JSON 與 SQLite Schema 維持不變

.NET 的一個 Tick 是 100 奈秒。本專案把 DateTimeOffset 以 Unix millisecond 寫進 SQLite,因此只差一個 Tick 的兩筆時間,經過資料庫寫入再讀回的 Database Round-trip 後,可能落在同一個毫秒。

我故意保留這個精度陷阱。它不只測試 TDD 與 TCR 會不會忠實執行 Tests,也能讓我看見:測試資料寫錯時,嚴格紀律究竟會怎麼反應。

這六種壓力最後被拆成八項可執行的驗收條件,並原樣寫入三組共用 Prompt。三種節奏面對的是同一份產品答案,差異只落在測試何時介入。

三組候選都只能修改四個既有檔案:Production 的 OverdueWorkItemProcessor.csNotificationGateway.cs,以及 Tests 的 ProcessOverdueBehaviorTests.csWorkItemsApiFactory.cs。同時禁止新增套件、資料庫 Migration、Strategy、Factory、第二個 Processor、第二套 Model 或新架構層。

Migration 是以版本化方式改變資料庫結構。這次先排除 Schema 變更,避免資料庫風險干擾三種測試節奏的比較。

這些限制是為了隔離開發節奏,不代表正式專案永遠只能修改四個檔案。真實需求仍要依 Repository 情境決定合理範圍。

用事前契約、開發節奏與獨立 Oracle 避免 Agent 自己出題、自己驗收

三種節奏最後都會執行 Tests,但那些 Tests 也是候選的一部分。若 Agent 同時解讀需求、撰寫測試與宣布完成,就可能用同一份誤解驗證自己。因此主流程另外保留三道互相獨立的檢查。

時間點 關卡 負責回答的問題
Agent 動手前 事前凍結八項行為 什麼答案才可以接受?
Agent 開發中 Direct、TDD 或 TCR 的流程規則 何時能改 Production、保留修改或退回?
Agent 完成後 Host Oracle 與完整主流程驗證 候選能否通過一份不由自己撰寫的驗收?

今天使用的 Host Oracle,是由 Agent 工作階段之外的主流程事先準備、完成後才注入的獨立驗收測試。它不依賴候選新增的方法名稱與類別結構,只檢查事前固定的外部行為。

主流程還會逐一執行套件還原、Release Build、完整 Tests、格式檢查、Diff 檢查與 API 基本流程驗證(Smoke Test)。Agent 的完成摘要只作過程紀錄,九份候選最後都要接受相同驗證。

三組 Prompt 共用需求與範圍,只改開發節奏

三組固定使用 Codex GPT-5.6-SOL-HIGH,每次都從相同 Commit 建立全新 Worktree,也不沿用前一次對話。

共同 Prompt 先固定需求、允許檔案與最終驗證:

固定起點:
- Commit:fc06aa4f02840c0afb5008043b5f219787db8fb9
- Annotated Tag:day-11-clean-classes
- 模型:gpt-5.6-sol/high

需求:
1. High 且 DueAtUtc <= currentUtc - 24 小時,呼叫 SendEscalationAsync。
2. 剛好 24 小時必須升級;少一個 Tick 維持 SendOverdueAsync。
3. 非 High 一律維持 SendOverdueAsync。
4. 已是 Overdue 的符合項目重跑時,仍使用相同通知路徑。
5. 升級通知沿用 Attempt、Failure 與失敗 ID 順序。
6. 通知回傳 false 時仍儲存 Overdue;Exception 與 Cancellation 不變。
7. 通知仍發生在 SaveChangesAsync 之前。
8. HTTP Route、Status Code、Response JSON 與 SQLite Schema 不變。

允許範圍:
- Production:OverdueWorkItemProcessor.cs、NotificationGateway.cs
- Tests:ProcessOverdueBehaviorTests.cs、WorkItemsApiFactory.cs
- 不得新增檔案、套件、Migration、Strategy、Factory、第二個 Processor、
  第二套 Model 或新架構層。
- 不得修改既有測試期待值來配合 Production Code。

最終驗證:
dotnet restore .\AiCleanCode.sln --locked-mode -p:NuGetAudit=false
dotnet build .\AiCleanCode.sln --no-restore --configuration Release
dotnet test .\AiCleanCode.sln --no-build --configuration Release
dotnet format .\AiCleanCode.sln --verify-no-changes --no-restore
git diff day-11-clean-classes...HEAD --check
.\scripts\run-series-baseline-smoke.ps1

三份 Prompt 只有以下開發節奏不同。

直接完成:Production Code 與 Tests 一次完成

1. 可以先閱讀與規劃,再一次完成 Production Code 與 Tests。
2. 不建立刻意的 Red 階段,也不建立中途 Commit。
3. 全部完成後才執行完整驗證。
4. 驗證通過後建立一個 Commit。

這組模擬大量 AI Coding 常見的做法:把整項需求交給 Agent,完成後再檢查 Tests 與 Diff。Direct 是本次實驗的控制組,不代表品質一定較差。

TDD:有效 Red 出現後才能修改 Production Code

Red:
1. Production Code 完全不能先改。
2. 先加入一個行為測試與最少的測試 Gateway 能力。
3. 測試必須辨認 24 小時等號邊界、一般通知與非 High 路徑。
4. 測試要因 Production 尚未路由升級通知而失敗。
5. 編譯錯誤、環境故障或無關斷言(Assertion)失敗都不算有效 Red。

Green/Refactor:
1. Red 確認後才允許修改 Production Code。
2. 只寫足以通過新測試的實作,再執行新測試與完整測試。
3. Green Commit 後才檢查命名、重複與可讀性。
4. 沒有具體整理理由就停止,不建立空的 Refactor Commit。

斷言(Assertion)是測試對結果提出的明確期待。若測試因套件、權限或編譯問題失敗,它沒有說明需求缺口,不能拿來充當 TDD 的 Red。

TCR:每一小批測試通過才 Commit,失敗就退回

至少切成三個可以獨立通過測試的 Green 小批次:
1. 建立 Gateway 能力與相容的測試 Gateway。
2. 加入最小升級路由。
3. 加入升級、24 小時邊界、一般通知與重跑測試。

每一批的規則:
- 通過:檢查暫存內容後立即 Commit。
- 失敗:保存狀態、失敗測試與明確檔案的 Diff。
- 接著逐一對明確檔案執行 git restore,回到目前 HEAD。
- 還原後先確認 Worktree 乾淨,再把批次切小重新嘗試。
- 不得使用 git reset --hard、git clean、目錄級還原或萬用字元。

三份未縮寫 Prompt、九個原始 Session 與主流程輸出都保存在 Day 12 公開實驗資料

九次執行的既有行為都通過獨立驗收,但第三次 TCR 沒有完成整項流程

三種節奏各執行三個彼此獨立的 Codex Session,共九次實驗。結果先放在同一張表裡:

節奏 三次執行結果 過程證據 批次與還原範圍 主流程獨立驗證
Direct 三次都完成需求 沒有刻意保留有效 Red 每次只有一個 Commit,涵蓋 231~248 行 三次皆通過
TDD 三次都先取得有效 Red,再完成 Green Red 前 Production Diff 都是 0 行 完整修改量為 125~136 行 三次皆通過
TCR Run 01/02 完成三個 Checkpoint;Run 03 發生兩次回復並停止 每一批都有測試與 Commit/回復紀錄 Run 01/02 的第三批仍有 212/217 行;Run 03 停在兩個 Green Commit 三份 Production 行為皆通過;Run 03 沒有宣告完成

Host Oracle 通過,只能證明 Run 03 已完成的 Production 行為符合事前契約。它沒有完成預定的第三個測試批次,因此這次執行應記為「依規則停止」,不能記為「整項任務完成」。

這不是把停止美化成成功。它只是誠實區分兩件事:已保留的兩個 Checkpoint 是綠的,但整項交付仍然缺少第三批測試。

Direct 三次都完成需求,但每次只有一個可還原的 Commit

三份 Direct 候選都正確完成需求,也不需要人工修補。直接完成仍然可能產生正確、可接受的 Code。

問題不在最終功能,而在失敗時缺少中途還原點。若 24 小時邊界寫錯,User 必須重新檢查涵蓋 Production、測試 Gateway 與行為測試的整份 Diff,再決定哪些內容能留下。

Direct 最後新增的 Tests 可以確認完成後的 Code 通過目前案例;它沒有留下實作前的有效 Red,因此無法回答 Tests 是否曾在舊 Production 上看見需求缺口。

查看 Direct Run 02 完整 Diff

TDD 三次都在 Production 零修改時取得有效 Red

三次 TDD 都先只修改 ProcessOverdueBehaviorTests.csWorkItemsApiFactory.cs。當時兩個允許的 Production 檔案沒有任何 Diff,新測試則因 EscalatedWorkItemIds 仍是空集合而失敗。

這三個 Red 都符合四項條件:

  • 測試可以編譯與執行。
  • 失敗原因直接指向尚未存在的升級路由。
  • Production Code 尚未修改。
  • Agent 沒有削弱既有 Assertion 來換取綠燈。

確認 Red 後,Agent 才加入 SendEscalationAsync 與通知選擇條件:

var currentUtc = timeProvider.GetUtcNow();
var dueIncompleteWorkItems = await FindDueIncompleteWorkItemsAsync(
    currentUtc,
    cancellationToken);
var processingSummary = await ProcessDueIncompleteWorkItemsAsync(
    dueIncompleteWorkItems,
    currentUtc.AddHours(-24),
    cancellationToken);

在單筆處理時,再用同一個時間快照算出的門檻選擇通知:

var notificationSucceeded = string.Equals(
        workItem.Priority,
        "High",
        StringComparison.Ordinal) &&
    workItem.DueAtUtc <= escalationThresholdUtc
        ? await notificationGateway.SendEscalationAsync(workItem, cancellationToken)
        : await notificationGateway.SendOverdueAsync(workItem, cancellationToken);

這段實作只擷取一次 currentUtc,查詢「是否已到期」與判斷「是否滿 24 小時」共用同一個時間基準。若每筆各自呼叫 GetUtcNow(),門檻附近的項目可能在同一批處理中讀到不同答案。

這次三份 TDD 候選的完整修改量為 125~136 行,少於 Direct 的 231~248 行;不過差異也受到測試組織方式影響,不能推論 TDD 固定能減少一半修改量。能直接確認的是:三次 TDD 都先讓舊 Production 因缺少目標行為而出現 Red。

查看 TDD Run 02 完整 Diff

TCR 雖然建立三個 Commit,最後一批仍包含兩百多行修改

TCR Run 01 與 Run 02 都完成三個 Green Checkpoint:

  1. Gateway 介面與測試替身具備升級通知能力。
  2. Processor 加入最小升級路由。
  3. 補齊升級、24 小時邊界、一般通知與重跑測試。

前兩步確實很小,也各自維持 Green。第三個 Checkpoint 分別增加 212 與 217 行,雖然可以獨立還原,仍然稱不上小批次。

這次結果顯示,三個 Commit 只代表流程留下三個還原座標,不能證明每一批都足夠小。還要檢查每批修改了哪些檔案、包含幾項行為,以及失敗時能不能整批放棄。

下一次執行 TCR 前,可以先替每個 Checkpoint 訂出行為範圍與停止條件,例如只處理一個邊界情境,或在 Diff 超過預期時先停止審查。這些數字應當是警報,不是固定品質分數。

查看 TCR Run 02 完整 Diff

第三次 TCR 因錯誤邊界資料停止,但已完成的通知路由通過獨立驗收

TCR Run 03 沒有走到第三個 Commit,中間發生兩次回復。

第一次是測試 Runner 的篩選參數不相容,實際執行到 0 個測試。Agent 保存狀態後,依規則放棄該批修改。

第二次出在邊界測試資料:

var atBoundary = currentUtc.AddHours(-24);
var oneTickBeforeBoundary = atBoundary.AddTicks(1);

測試把 currentUtc 放在整毫秒。SQLite 儲存 DateTimeOffset 後,兩筆只差一個 Tick 的資料落在同一個毫秒,測試因此誤判一般通知路徑。Agent 再次放棄失敗批次,最後停在兩個乾淨的 Green Commit。

主流程事前凍結的 Host Oracle 把固定時間放在下一毫秒前一個 Tick:

var utcNow = new DateTimeOffset(2026, 9, 1, 8, 0, 0, TimeSpan.Zero)
    .AddTicks(TimeSpan.TicksPerMillisecond - 1);

這樣「剛好 24 小時」與「少一個 Tick」經過 Database Round-trip 後,仍會落在相鄰毫秒。主流程注入這份 Oracle 後,Run 03 已完成的 Production 路由全部通過。

TCR 正確執行了 Commit 與回復規則,卻因測試資料錯誤而停止後續工作。這表示流程紀律只能忠實執行目前的 Oracle;User 仍要確認時間精度、邊界值與測試資料是否真的代表產品規則。

查看停在兩個 Green Commit 的 TCR Run 03 Diff

九次執行沒有顯示 TDD 或 TCR 能穩定節省 Token

Fresh Input Token 是扣除快取後,Agent 在該 Session 重新讀入的輸入量;Tool Calls 則是 Agent 呼叫檔案、終端機與其他工具的次數。

九次量測資料整理成範圍後如下:

節奏 Fresh Input Token Tool Calls 執行時間
Direct 113,978~175,742 26~53 427.21~615.32 秒
TDD 125,762~139,680 31~50 644.64~708.26 秒
TCR 109,027~236,214 41~71 435.13~698.75 秒

這九次執行沒有出現「紀律愈嚴格,Token 就愈少」的穩定趨勢。TDD 多了 Red 階段,TCR 則增加測試、Commit 與回復操作。

Direct 與 TDD 使用 workspace-write,也就是只能在指定工作區內寫入的受限模式;TCR 為了寫入 Worktree 的 Git 內部資料,使用允許較廣泛本機檔案操作的 danger-full-access。三組的權限環境不同。

因此,這些數字只能描述本次執行成本,不能用來判定哪一種節奏天生比較省 Token、工具呼叫或時間。

完整九次量測資料、原始輸出與驗證結果收錄在 Day 12 實驗控制文件

測試全部通過後,還要檢查需求缺口、獨立驗收與可還原範圍

AI Agent 很容易回報「新增測試,全部通過」。這句話只能說明目前執行的案例是綠燈,還不足以回答測試在開發過程中發揮了什麼作用。

我會再追查五個問題:

檢查項目 審查時要看什麼
行為涵蓋 需求、等號邊界、失敗、副作用與重跑是否都有可執行案例?
缺口辨識 新測試是否曾在舊 Production 上因正確理由失敗?
Oracle 獨立性 驗收條件是否在 Agent 動手前固定,或由另一個流程重新驗證?
失敗定位 紅燈能不能直接指出漂移的行為?
還原能力 失敗時要放棄幾個檔案、多少行,以及能回到哪個 Green Checkpoint?

Coverage 可以協助找出完全沒走到的區域,測試數量也能描述規模。兩者都無法單獨確認 Assertion 是否正確、邊界資料是否可靠,或測試是否真的限制了需求。

CLEAN 原則:L 限制失敗批次,A 保留抵達綠燈的證據

L — Localized Change 局部變更:用失敗時要放棄多少內容衡量批次

Direct 三份候選都只有一個 Commit;TCR Run 01 與 Run 02 都有三個 Commit。只數 Commit,TCR 看起來一定比較局部,可是第三個 Checkpoint 仍超過兩百行。

L — Localized Change 局部變更 在這裡檢查三件事:這一步承擔哪個行為、失敗時要放棄多少內容,以及能否回到上一個可工作的狀態。

User 應先替每一批寫清楚行為範圍、允許檔案、通過條件與還原方式。Agent 可以自主決定實作細節,但每個 Checkpoint 都必須能單獨理解、驗證與放棄。

A — Auditable by Evidence 實據可審:全綠之外,還要留下抵達綠燈的過程

Direct、TDD 與 TCR 的 Production 行為最後都通過 Host Oracle,三種節奏留下的過程證據卻不同。

A — Auditable by Evidence 實據可審 要求我保留足以回答決策的資料:

  • Agent 是否先修改了不該先動的 Production?
  • Red 是否因目標行為尚未存在而失敗?
  • 失敗批次到底有多大?
  • Agent 是否削弱測試來換取綠燈?
  • Agent 有沒有依流程停止?
  • 最終候選能否通過事前固定的 Host Oracle?

把 Prompt、Diff、測試時序、Commit/回復、主流程 Gate 與人工接受理由放在一起,才能審查 Agent 的開發過程。只有一句 Tests Passed,無法回答 Agent 是否先改了 Production、Red 是否有效,或失敗時究竟退回了哪些內容。

依需求風險、還原範圍與測試速度選擇 Direct、TDD 或 TCR

今天的三種節奏沒有固定冠軍。我會按照需求風險、可還原範圍與測試速度選擇。

小型、低風險而且整包放棄也不痛:可以直接完成

例如局部文案、明確 Mapping,或已有可靠回歸測試(Regression Tests)的小修改。回歸測試用來確認這次變更沒有破壞原本已成立的行為。

任務仍要固定允許範圍、不得改變的契約與最終 Gate。Diff 開始超出預期時,就停止並改用較小週期。

Direct 少了 Red 與中途 Checkpoint,流程步驟較少;相對地,還原點也較少。它適合整包放棄仍可接受的小修改,不適合把付款、權限、時間邊界或多項副作用塞進同一次交付。

業務規則、邊界值與回歸風險明確:優先考慮 TDD

像今天的 24 小時等號邊界、通知失敗仍要儲存,以及重跑時的通知路徑,都能先寫成可觀察行為。User 先確認 Red 的原因,Agent 再進入 Green,可以降低 Production 與 Tests 一起誤解需求的機率。

TDD 未必節省 Token,還會多一次 Red 執行與紀錄。我願意在付款、權限、狀態轉換、時間邊界與資料一致性需求上支付這個成本,因為 Red 能留下清楚的需求缺口與因果順序。

TDD 的 Red、Green、Refactor 會增加前置測試、執行與紀錄成本,這也是不少團隊過去對它有所保留的原因。進入 AI Coding 後,我反而更願意支付這段成本:人可以不親手輸入每一行 Code,仍要先把「什麼答案才正確」變成 Agent 能執行的測試。對我來說,這已經是使用 AI Coding 時不可或缺的能力。

官方指南不一定要求完整 TDD,方向卻很一致。GitHub 建議在 Repository Instructions 寫清楚建置、測試與驗證步驟。Anthropic 要求提供可執行的 Verification Criteria,也示範先寫出能重現問題的 Failing Test。OpenAI 則建議 Coding Agent 在修改後執行相關單元測試、型別或格式檢查、Build 與必要的 Smoke Test,無法執行時要說明原因。

三份指南的共同點,是 Agent 必須實際驗證結果,不能拿完成摘要代替證據。

高風險只是起點:任務能切小、測試夠快、Git 可安全還原時才考慮 TCR

TCR 最適合先當成練習小步開發與快速回饋的流程實驗。murex/TCR 專案也提醒,這套方法有助於練習 Baby Steps,直接套用到正式 Production Code 則更具挑戰。因此我不會只因需求風險高,就直接把所有開發升級成 TCR。

我會在修改失敗的代價很高,而且每一步都能由 Git 安全還原時,才把節奏收緊成 TCR。例如:

  • 權限或資料範圍判斷出錯,可能讓不該看見資料的人取得存取權。
  • 付款金額、折扣、帳務或資料一致性規則出錯,可能留下難以追查的錯帳。
  • 併發、冪等性(Idempotency,同一操作重試多次仍只產生一次預期效果)或狀態轉換出錯,可能造成重複執行或非法狀態。
  • 公開 API Contract 或多人共用的核心元件正在重構,中間版本都應維持既有呼叫端可用。
  • 大型既有系統已經具備可靠 Regression Tests,希望 Agent 只保留通過驗證的小步修改。

這些情境還要同時滿足三個條件:任務可以切成幾分鐘內完成的小步、測試能快速且穩定地指出錯誤、上一個 Green Commit 足以還原這批修改。缺少其中一項,TCR 只會增加 Commit、回復與工具呼叫。

Git 只能還原 Repository 內的檔案,不能收回已執行的資料庫 Migration、已送出的通知、付款或外部 API 寫入。

這些外部副作用還需要其他保護方式,例如在 Sandbox 隔離環境演練、先做不寫入真實系統的 Dry Run、準備 Rollback 方案,或執行一筆抵銷原操作的補償操作(Compensating Action)。完成後還要獨立查核外部狀態。TCR 能保護程式碼修改歷程,無法替外部世界倒帶。

24 小時邊界需求採用第二次 TDD 結果,其他情境仍依風險重新選擇

回到這次需求,它同時具有業務規則、精確時間邊界、通知副作用與重跑行為,因此我採用 TDD。

  • Direct 三次都做對功能,但單一 Commit 的還原範圍仍有 231~248 行,也沒有留下有效 Red。
  • 這項需求有清楚的等號邊界與回歸風險,適合先用 Red 確認缺口。
  • TCR 的紀律更緊,Run 01/02 的第三批卻超過兩百行,本次沒有因為增加 Commit 就得到真正的小批次。

三份 TDD 都有有效 Red,我最後採用 Run 02:

  • TDD Run 01 與 Run 03 都會在單筆處理時再次讀取 TimeProvider,門檻附近的多筆資料可能取得不同的現在時間。
  • TDD Run 02 只擷取一次 currentUtc,到期查詢與升級門檻共用同一份時間快照。
  • Run 02 的測試把固定時間放在毫秒末端,讓「少一個 Tick」經 SQLite Database Round-trip 後仍可辨識。
  • Host Oracle、Build、完整 Tests、Format、Diff 與 Smoke 全部通過。

想重現實驗時,可以先切換到相同起點

讀者不必下載專案,也可以直接閱讀本文的 Prompt、結果與公開 Diff。若想從相同基準重現實驗,可以執行:

git clone https://github.com/eric861129/AI-CleanCode-API-Demo.git
Set-Location .\AI-CleanCode-API-Demo
git fetch origin --tags
git switch --detach day-11-clean-classes
git rev-parse HEAD

最後一行應該顯示:

fc06aa4f02840c0afb5008043b5f219787db8fb9

系列接續版本

採用版本如下:

  • Commit:1f2f818ea677ee4f6c867aa86e21f94f40da5b61
  • Annotated Tag:day-12-testing-disciplines
  • 完整 Diff:新增 128 行、刪除 8 行

可以直接切換到這個版本:

git fetch origin --tags
git switch --detach day-12-testing-disciplines
git rev-parse HEAD

最後一行應該顯示:

1f2f818ea677ee4f6c867aa86e21f94f40da5b61

Run 02 因為共用單一時間快照,並讓毫秒精度下的邊界資料保持可辨識,所以成為系列接續版本。若未來需求改成低風險 Mapping,或進入付款、權限等更高風險情境,仍應重新評估 Direct、TDD 或 TCR。

把三種開發節奏整理成 Testing Discipline Policy 範例

我不希望每次都重新告訴 Agent 什麼叫有效 Red、何時可以修改 Production,以及失敗後要怎麼回復。以下先把今天的校準結果整理成一份可放入 AGENTS.md 的 Policy 範例:

## Testing Discipline Policy

### 共同前置條件

- 開始實作前,先列出需求行為、等號邊界、失敗路徑、副作用順序與不得改變的契約。
- 高風險行為由 User 或主流程事前凍結獨立 Oracle;Agent 不得以自己的最終測試取代 Oracle。
- Agent 先依需求風險、可還原範圍與測試速度建議 Direct、TDD 或 TCR,並列出判斷理由。
- Repository 或 User 已指定節奏時,依指定流程執行;無法滿足條件時停止並回報。

### Direct

- 只用於低風險、既有 Regression Tests 足夠,而且可以整批放棄的小修改。
- 先限制允許檔案、不得改變的契約、最終 Gate 與單一 Commit 的最大範圍。

### TDD

- 業務規則、邊界值、狀態轉換或回歸風險明確時,優先考慮 TDD。
- Red 前不得修改 Production Code;Red 必須能編譯、能執行,並因缺少目標行為而失敗。
- 不得以削弱 Assertion、刪除案例、排除測試或改變既有期待值取得 Green。
- Green 只加入足以通過目標行為的修改;完整 Regression Tests 通過後才進入 Refactor。

### TCR

- 失敗代價高、任務能切成數分鐘的小步、測試快速可靠,而且每批都能由 Git 安全還原時,才採用 TCR。
- 每一批都從乾淨的 Green 狀態開始;通過就 Commit,失敗先保存測試輸出與 Diff。
- 失敗後逐一對明確檔案執行 git restore,回到目前 HEAD。
- 不得使用 git reset --hard、git clean、目錄級還原或萬用字元處理失敗批次。
- Commit 數量不代表 Small Bundles;每個 Checkpoint 都要回報行為、檔案、Diff 行數與可獨立還原的理由。

### 共同驗收

- 時間、精度、排序、文化特性與 Database Round-trip 的邊界測試,必須確認測試資料在實際 Provider 中仍可辨識。
- Agent 完成後,由主流程重新執行相同 Oracle、Build、完整 Tests、Format、Diff Check 與必要 Smoke。
- 最終回報包含 Red/Green 或 Commit/回復時序、失敗原因、人工修補與停止理由,不能只回報最後全綠。

這份 Policy 目前是文章中的示範,尚未寫入 Demo Repository 的 AGENTS.md。未來會再抽成獨立 SKILL,方便重複使用、版本控制與開源;讀者現階段可以先依自己的 Repository、測試速度與 Git 流程調整後使用。

測試介入的時間,決定未驗證修改會累積多少

回到標題,寫完再測與先測再改的差別,不是其中一種一定能寫對、另一種一定會寫錯。九次實驗裡,Direct 也能交出正確 Code。

真正的差異是:需求缺口何時第一次被看見、尚未驗證的修改會累積多少,以及失敗時有沒有清楚的回復位置。

這項 24 小時邊界需求最後採用 TDD Run 02,因為它先留下有效 Red、共用單一時間快照,也通過主流程獨立驗收。實驗沒有顯示 TDD 或 TCR 能穩定節省 Token;它們增加的是需求缺口、失敗位置與還原範圍的可見性。

這正好對應 L — Localized Change 局部變更A — Auditable by Evidence 實據可審:User 先限制一次能放棄的範圍,再保留 Agent 如何抵達綠燈的證據。

系列第一篇提到的 Uncle Bob 兩則 X 貼文,也把測試、Coverage、Mutation Testing、Gherkin、品質指標與 QA Procedure 放進 Agent 的約束流程。單次 Tests 全綠只是其中一項,還不能代表整套驗證可信。

更麻煩的問題是:即使測試介入得很早,測試資料本身也可能答錯。

明天將繼續檢查〈整潔的測試〉與〈驗收測試〉:如果 Tests 命名混亂、彼此耦合,或使用錯誤的邊界資料,什麼樣的測試才值得交給 Agent 反覆執行?

參考資料


上一篇
Day 11|類別低於 100 行就只有一個責任嗎?用高優先逾期通知比較行數上限與內聚拆分
下一篇
Day 13|測試全綠,為什麼仍漏掉「剛好 24 小時」?用整潔測試、驗收測試與受控 Mutation 檢查答案
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言