iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
AI Engineering

轉生到 AI 世界,帶著兩個 AI 隊友獨自進化系列 第 10 篇

Day 10|先寫會失敗的測試:TDD 在 AI 協作下的角色

  • 分享至 

  • xImage
  •  

同一筆「休息」,在兩個模組裡有不同意思

IMS 的排班資料裡,有一種任務叫「休息(10)」。BlockBuilder 負責把同一人的時段整理成任務區段,其中一個測試是:

// 休息時段夾在同 role 中間 → 合併為一個 block
func testAbsorbsBreaksWithinSameRole() {
    let tasks = [
        raw("外場負責人", "D1", "9:40", "10:20", content: "session(40)"),
        raw("外場負責人", "D1", "10:20", "10:30", content: "休息(10)"),
        raw("外場負責人", "D1", "10:30", "10:50", content: "session(40)"),
    ]
    let blocks = BlockBuilder.blocks(for: "X", in: tasks)
    XCTAssertEqual(blocks.count, 1)
    XCTAssertEqual(blocks[0].start, "9:40")
    XCTAssertEqual(blocks[0].end, "10:50")
}

這是專案測試的節錄,raw 是建立測試資料的輔助函式。案例表達的規則很明確:同一角色中間有排班表明列的休息,仍合併成 9:40–10:50 的一個區段,方便安排一次提醒。

後來,「現場全覽」卻遇到另一個問題。負責整理全場名冊的 RosterBuilder 排除了休息資料,結果在休息時段把人標成空閒。但就崗位分配來說,中控仍是中控,外場仍是外場,不能因為這段標成休息,就把人從角色名單移除。

同一筆資料,在兩個模組中服務不同目的:BlockBuilder 要決定提醒區段,RosterBuilder 要呈現人員所在的角色。修正後者時,前者的測試仍通過,讓我能確認既有的合併案例沒有被改壞。

測試把模組各自的規則寫了下來,讓修改時有東西可以對照。

TDD 在 AI 協作裡,也是一種溝通方式

TDD 是測試驅動開發:先寫出預期行為的測試,確認目前還做不到,再寫實作讓它通過。

在 AI 協作裡,測試還能降低文字需求的歧義。比如「時間用台北時區」,可以搭配一個具體例子:

// D1 08:00 台北,對應 2026-07-25T00:00:00Z
// 此處 fmt 是測試中設為 UTC 的 formatter。
XCTAssertEqual(fmt.string(from: block.startDate), "2026-07-25T00:00:00Z")

這個斷言明確列出輸入情境與預期時間。它沒有規定內部只能怎麼寫,但實作必須符合這項結果。

不過,測試本身也可能寫錯。如果 AI 同時根據一個錯誤假設寫測試和實作,兩者仍可能一起通過。因此,Day 9 要先由人檢查測試案例;到了實作階段,則要真的觀察它從失敗變成通過。

本篇練習的紀律是:先看到預期的失敗,再寫讓測試通過的實作。 如果 AI 提前交出兩者,不要把那次綠燈當成完整的 TDD 證據;先把新增實作與測試分開檢查,再確認缺少該行為時測試會失敗。

Red–Green–Refactor:每一步都留下證據

TDD 的循環分成失敗(Red)、通過(Green)和重構(Refactor):

階段 要做什麼 要檢查的證據
Red 寫一個測試,描述目前尚未滿足的行為。 測試程式碼與預期結果。
確認 Red 執行測試,閱讀失敗原因。 實際失敗輸出,確認失敗與目標行為有關。
Green 寫足以滿足該行為的實作。 實作差異。
確認 Green 重跑測試,也檢查受影響的既有行為。 執行範圍、測試結果與完整日誌。
Refactor 整理命名或重複邏輯,不增加新行為。 整理後的差異,以及測試仍通過的結果。

最容易被跳過的是「確認 Red」。AI 說「這應該會失敗」,不代表它已經執行過;即使真的失敗,也可能只是測試資料載入錯誤。

如果新 API 還不存在,第一次可能先遇到編譯錯誤。要分清楚「尚未建立介面」和「測試程式碼寫錯」,必要時先補最小介面,再確認行為測試真的會失敗。

九月初,我在一個遊戲專案遇到過一個錯誤:介面已把玩家選擇的行動寫進草稿,送出後卻變成「休息」。先補上測試,得到的失敗訊息是 summary still 這回合:休息。這行輸出留下了可重現的問題,修正後再確認它轉綠,就比只說「看起來修好了」明確得多。

先測邏輯,再安排其他驗證

IMS 的單元測試重點放在任務合併、時間軸切片、通知時間計算和名冊分組。這些邏輯可以用明確的輸入與輸出檢查。

例如,NotificationPlan.fireDate(for:) 回傳預定的通知時間,適合直接測;把通知交給 UNUserNotificationCenter 後,系統是否按預期觸發,則還需要整合或真機驗證。

我的做法是盡量把計算抽成純函式:給定相同輸入,就得到相同輸出,不在裡面讀取外部狀態或發送通知。IMS 把 App 與 Widget 共用的邏輯放在 Shared/,也方便測試。

這不代表 UI 或副作用不能測,而是先把不同驗證責任分開。單元測試確認計算規則,其他測試與實機檢查再確認畫面、系統互動和使用體驗。

今天要做的一件事:完成一次 Red–Green,再確認測試能抓錯

從 Day 9 的計畫挑一個有明確輸入與輸出的邏輯 task,使用下面的指令:

執行 <Task>,這次分階段讓我檢查:
1. 先寫測試並執行,提供測試程式碼和失敗輸出。
2. 我確認失敗原因符合預期後,再寫最小實作。
3. 執行新測試與相關既有測試,提供輸出。
4. 如有需要,整理重複邏輯或命名,再次執行測試。
每個階段等我看完再繼續;完整日誌要保留。

完成後,再做一個小檢查:暫時引入一個應該被目前測試抓到的錯誤。例如,測試涵蓋開始時間的邊界,就把 < 改成 <=;測試涵蓋人員篩選,就暫時拿掉對應條件。

如果相關測試失敗,代表它能偵測這種錯誤;若仍通過,就檢查測試資料與斷言是否真的覆蓋了該行為。這不能證明測試已抓到所有錯誤,但能揭露一部分盲點。

最後要還原這個刻意引入的錯誤,重跑測試確認通過,再交付結果。

新人最常在這一步犯的錯

測試和實作一起交,只看綠燈。 先檢查預期行為,再確認缺少它時測試確實會失敗。

看到失敗就繼續,沒看原因。 路徑錯誤、資料載入失敗和行為不符,需要不同的處理。

把所有驗證都塞進 UI 測試。 能直接檢查的計算邏輯,先用單元測試驗證,避免只能透過畫面間接猜測。

替身太多,看不出業務行為。 能用合成資料直接呼叫邏輯時,就先這樣測;需要隔離外部系統時,再使用 mock 等測試替身。

改壞實作後,忘了還原。 驗證測試能抓錯只是暫時實驗,完成後必須恢復正確實作並確認結果。

本篇可帶走的檔案

第一份是 tdd-prompt.md:

執行 <Task>,每個階段先交給我檢查:

1. 針對一個行為寫測試,優先用可控的合成資料。
2. 執行並提供失敗輸出,說明失敗是否符合預期。
3. 經確認後寫最小實作,再跑新測試與相關既有測試。
4. 如需重構,保持行為不變,完成後重跑測試。
5. 保留完整日誌,回報執行範圍、結果與尚未驗證的部分。

不要只說「應該會失敗」;不要為了通過而放寬原本正確的斷言。
如果發現測試本身有誤,先說明原因並重新確認預期行為。

第二份是 implementation-log-entry.md。除了規格、決策與計畫,實作日誌補上了「實際發生過什麼」:

## <日期> — <Task>/<一句話標題>

- **先寫的測試與觀察到的失敗**:<測試名稱、失敗訊息原文>
- **轉綠的證據**:<指令、測試範圍、結果與日誌路徑>
- **與 Ticket/規格的差異**:<無,或經確認的變更>
- **失敗的嘗試**:<試了什麼、為什麼不行>
- **取捨**:<選了什麼、放棄了什麼>
- **影響的檔案與測試**:
- **刻意引入錯誤的檢查(若有)**:<測試反應、是否還原並重驗>

保留失敗嘗試的原因,能讓下次接手的人少走同一條彎路。日誌不必抄下整段對話,但要留下足以理解結果的證據。


上一篇
Day 9|Plan 是外接記憶
系列文
轉生到 AI 世界,帶著兩個 AI 隊友獨自進化 共 10 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言