T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞
T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞
Stub 回放的是測試作者的假設,假設錯了,測試照樣綠燈——這是 stub 的結構性限制。當 bug 的成因是「我們對後端行為的理解錯誤」時,用自己假設餵出來的 stub 永遠驗證不出來。
觀察
前端的本地追蹤記錄裡凍結了兩個後端 id:事件發生當下所屬單據的 id、與明細列的 id。這是一個前後端分離的 APP 專案,前端為後端推送的每個事件建立這樣一筆記錄,後續的「取消」「追加」操作用這兩個凍結 id 回寫後端。
後端有一個「合併兩張單據」的操作。實際行為是:建立一張新單據、明細列全部重建(全新 id)、刪除舊單據。合併後,本地記錄裡凍結的舊 id 全部指向已刪除的資料——取消操作永遠失敗、追加操作無聲失效。
| 指標 | 值 |
|---|---|
| 影響範圍 | 合併操作後,該單據所有品項的取消與追加功能全部失效 |
| 單元測試結果 | 全部通過 |
| 遮蔽機制 | 測試把「回寫目標存在」的狀態直接餵給 stub,凍結 id 在測試裡永遠有效 |
| 修復 | 動作時活解析:以跨合併不變的事件 id 反查後端資料的當前快照,取得現在有效的 id;凍結值只做查無時的退路 |
判讀
凍結 id 的生死由後端決定,stub 卻由前端決定。單元測試建立記錄時,順手把對應的單據與明細餵進 stub——凍結 id 與 stub 資料出自同一隻手、恆常一致。真實世界裡讓 id 死亡的是後端的重建行為,stub 沒有這個行為,於是「id 失效」這個狀態在測試裡不可能出現。
修復方向也受同一個盲區支配。第一版修復只搬了單據層的 id(合併後通知本地記錄改掛新單據),沒人想到明細層的 id 也死了——因為測試裡明細 id 從未死過。盲區不只遮蔽 bug,還遮蔽修復的完整性。
判斷關鍵:哪個 id 跨越操作仍不變。事後分析發現後端合併時保留了事件記錄的 id(只改外鍵)。「什麼會變、什麼不變」是後端行為的事實問題,推理與記憶都不算出處——合法的出處是可驗的契約文件、或一次實測取證,然後把證實的行為固化進測試環境。
策略
- 對「參照外部資料 id」的功能,測試必須包含「id 失效」的劇本。凡是前端持有後端 id 的地方,就要問:後端的哪些操作會讓這個 id 死亡?該劇本進測試。
- 用有狀態的假後端取代由測試餵資料的 stub:假後端自己模擬「合併=重建明細+換 id」的行為,前端的活解析邏輯就會在測試裡被真正逼出來。做法見語意級假後端。
- 實測後端行為一次、固化為測試資產。「合併後哪些 id 存活」這類問題用一次埋 log 實測定案,結論寫進假後端,之後不再依賴任何人的記憶。
下一步路由
- 想建立有狀態假後端 → 語意級假後端與流程測試
- 假後端的行為假設怎麼對真實後端驗證 → 真實後端驗證測試
- 換了測試形態後首跑就抓到問題的實例 → T.C6 流程測試首跑抓到順序 bug