iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Software Development

AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質系列 第 24 篇

Day 24:案例——一次 AI 寫測試掩蓋掉真正 bug 的真實除錯過程

  • 分享至 

  • xImage
  •  

前言:進入第四部,用真實案例把前面 23 天的原則串起來

從今天開始是這個系列的第四部:實戰案例與總結。前面 23 天講了不少原則——假陽性測試、過度 mock、邊界情境遺漏、循環被打斷。今天用一次除錯過程,看這些原則怎麼在真實情境裡一起發揮(或者說,沒有發揮)作用。

今日目標

  • 看一次真實除錯過程:一個生產環境的 bug,回頭發現測試早就該抓到卻沒抓到
  • 理解「有測試」跟「測試涵蓋了這個 bug 對應的情境」是兩件事
  • 學會除錯時回頭檢視測試套件,判斷缺口出在哪個環節
  • 建立一套事後補強測試的具體做法

案例:重複扣款的生產事故

一套訂單系統的付款流程,某天收到回報:少數訂單被重複扣款兩次。追查發現,問題出在「使用者在付款按鈕上快速點擊兩次」這個情境——第一次點擊的請求還在處理中,第二次點擊的請求就已經送出,兩個請求都各自完成了一次扣款。

回頭檢查這段付款邏輯的測試套件,發現測試確實存在,而且看起來寫得挺完整:

❌ 事故發生前的測試套件(節錄):
[Fact]
public void ProcessPayment_有效訂單_應成功扣款()
{
    var order = new Order(amount: 100, status: OrderStatus.Pending);
    var result = paymentService.Process(order);
    Assert.True(result.Success);
}

[Fact]
public void ProcessPayment_訂單已付款_應拒絕重複扣款()
{
    var order = new Order(amount: 100, status: OrderStatus.Paid);
    var result = paymentService.Process(order);
    Assert.False(result.Success);
}

第二個測試案例看起來就是在防重複扣款——但仔細看會發現,這個測試驗證的是「訂單狀態已經是 Paid 時再呼叫會被拒絕」,這是一個循序發生的情境:第一次扣款完全結束、狀態更新完畢之後,第二次呼叫才進來。真實事故裡發生的是兩個請求幾乎同時進來,第一個請求還沒把狀態更新成 Paid,第二個請求就已經讀到了舊的 Pending 狀態——這是一個並發情境,跟測試驗證的循序情境完全不同。

為什麼這個缺口沒有被發現

這正是 Day15 講過的「邊界情境容易被忽略」的真實樣貌——AI(或當初寫測試的人)想到了「重複扣款」這個規則需要被保護,也確實寫了一個測試,但這個測試只涵蓋了「循序重複呼叫」這一種情境,沒有涵蓋「並發情境下的重複呼叫」。兩者從業務規則的角度看是同一件事(防止重複扣款),但從程式碼實作跟測試設計的角度看,是完全不同的技術問題——循序重複可以靠檢查狀態欄位擋住,並發重複需要額外的鎖機制或幂等性設計。

測試套件裡「看起來已經測過重複扣款」這件事,製造了一種虛假的安心感——這正是這個系列反覆強調的主題句在這裡的具體樣貌:「已經測過重複扣款」這句話,其實只在「循序呼叫」這個查證範圍內成立,範圍外(並發呼叫)的情況完全沒被驗證過,卻被直覺地當成「這條規則已經被涵蓋了」。

事後補強:先寫一個真正重現問題的測試

修復這類 bug 的第一步,不是直接改程式碼,是先寫一個能重現「並發呼叫」情境的測試,讓它先紅——確認這個測試真的能抓到問題,再動手修正實作(用資料庫層級的鎖或幂等性 token 之類的機制),最後確認新測試變綠、舊測試依然通過。

✅ 補上的測試:
[Fact]
public async Task ProcessPayment_同時發出兩個請求_只有一個應成功()
{
    var order = new Order(amount: 100, status: OrderStatus.Pending);

    var task1 = Task.Run(() => paymentService.Process(order));
    var task2 = Task.Run(() => paymentService.Process(order));
    var results = await Task.WhenAll(task1, task2);

    Assert.Equal(1, results.Count(r => r.Success));
}

今日思考題

回想你手上系統裡某條「已經有測試保護」的重要規則:那個測試涵蓋的情境,跟真實世界可能觸發這條規則的所有情境,是不是完全一致?有沒有並發、時序這類容易被漏掉的維度?

今日重點回顧

  • 案例:重複扣款事故,回頭發現測試確實存在,但只涵蓋了「循序重複呼叫」,沒涵蓋「並發呼叫」
  • 「已經測過這條規則」這句話的可信度,取決於測試涵蓋的情境範圍,不是規則本身有沒有被想到
  • 同一條業務規則,可能對應多種完全不同的技術情境(循序 vs 並發),需要分別驗證
  • 修復流程:先寫一個能重現問題的測試讓它紅,再動手修正,最後確認新舊測試都通過

明日預告

明天要看另一種除錯情境:AI 把測試改鬆來讓 CI 通過,而不是真的修好程式碼——這是一種更隱蔽、更容易被忽略的失敗模式。


上一篇
Day 23:AI 協作下的 Pair Programming——人 navigator,AI driver 的分工模式
系列文
AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言