Stub 回放的是測試作者的假設,假設錯了,測試照樣綠燈——這是 stub 的結構性限制。當 bug 的成因是「我們對後端行為的理解錯誤」時,用自己假設餵出來的 stub 永遠驗證不出來。

觀察

前端的本地追蹤記錄裡凍結了兩個後端 id:事件發生當下所屬單據的 id、與明細列的 id。這是一個前後端分離的 APP 專案,前端為後端推送的每個事件建立這樣一筆記錄,後續的「取消」「追加」操作用這兩個凍結 id 回寫後端。

後端有一個「合併兩張單據」的操作。實際行為是:建立一張新單據、明細列全部重建(全新 id)、刪除舊單據。合併後,本地記錄裡凍結的舊 id 全部指向已刪除的資料——取消操作永遠失敗、追加操作無聲失效。

指標
影響範圍合併操作後,該單據所有品項的取消與追加功能全部失效
單元測試結果全部通過
遮蔽機制測試把「回寫目標存在」的狀態直接餵給 stub,凍結 id 在測試裡永遠有效
修復動作時活解析:以跨合併不變的事件 id 反查後端資料的當前快照,取得現在有效的 id;凍結值只做查無時的退路

判讀

  1. 凍結 id 的生死由後端決定,stub 卻由前端決定。單元測試建立記錄時,順手把對應的單據與明細餵進 stub——凍結 id 與 stub 資料出自同一隻手、恆常一致。真實世界裡讓 id 死亡的是後端的重建行為,stub 沒有這個行為,於是「id 失效」這個狀態在測試裡不可能出現。

  2. 修復方向也受同一個盲區支配。第一版修復只搬了單據層的 id(合併後通知本地記錄改掛新單據),沒人想到明細層的 id 也死了——因為測試裡明細 id 從未死過。盲區不只遮蔽 bug,還遮蔽修復的完整性。

  3. 判斷關鍵:哪個 id 跨越操作仍不變。事後分析發現後端合併時保留了事件記錄的 id(只改外鍵)。「什麼會變、什麼不變」是後端行為的事實問題,推理與記憶都不算出處——合法的出處是可驗的契約文件、或一次實測取證,然後把證實的行為固化進測試環境。

策略

  1. 對「參照外部資料 id」的功能,測試必須包含「id 失效」的劇本。凡是前端持有後端 id 的地方,就要問:後端的哪些操作會讓這個 id 死亡?該劇本進測試。
  2. 用有狀態的假後端取代由測試餵資料的 stub:假後端自己模擬「合併=重建明細+換 id」的行為,前端的活解析邏輯就會在測試裡被真正逼出來。做法見語意級假後端
  3. 實測後端行為一次、固化為測試資產。「合併後哪些 id 存活」這類問題用一次埋 log 實測定案,結論寫進假後端,之後不再依賴任何人的記憶。

下一步路由