金流系統怎麼測:我在 Go 裡寫測試的真實做法
在管錢的系統裡,測試不是「加分項」,是「能不能睡得著」的問題。一個沒測到的邊界,可能就是一筆算錯的帳。這篇講我在 Go 裡實際怎麼寫測試,特別是金流這種對正確性要求極高的場景,哪些該測、怎麼測、以及我不追求的東西。
我不追求覆蓋率數字
先講一個可能不討喜的觀點:我不把「測試覆蓋率 90%」當目標。覆蓋率高不代表測對了——你可以跑過一行卻沒驗證任何重要的行為。我在意的是關鍵路徑和邊界有沒有被認真測到,而不是數字好不好看。
對金流,我會把測試火力集中在:金額計算、狀態轉移、冪等、併發、以及各種「失敗的中間狀態」。
table-driven test 是 Go 的主場
Go 寫測試最自然、最好用的就是 table-driven:把一堆「輸入 → 預期輸出」列成一張表,用同一段邏輯跑過。它的好處是新增一個案例只要多一列,逼你把各種邊界一個個列出來。
金額計算尤其適合:正常值、零、負數、極大值、捨入邊界、跨幣別……每一個都是表裡的一列。列著列著,你常會發現自己漏想的情況。
善用 interface 來隔離外部依賴
前面講結構時提過,service 依賴的是小介面。測試時就把資料庫、外部金流 API 換成假的實作,專心測業務邏輯本身。這讓單元測試不需要真的連線、跑得飛快、也不會因為外部環境而時好時壞。
要注意:mock 是用來隔離不相關的依賴,不是用來測一切。涉及真實資料庫行為(交易、唯一約束、鎖)的東西,假物件測不出真相。
重要的東西要用真的資料庫測
冪等和對帳這類邏輯,靠的就是資料庫的唯一約束和交易行為,用 mock 測等於沒測。這種我會開一個真的資料庫(用容器起一個乾淨的實例)做整合測試,驗證:
- 同一個 idempotency key 併發進來,只會成立一筆
- 交易該回滾的時候真的回滾
- 唯一約束真的擋住重複
這類測試慢一點,但它測的是「會不會重複扣款」這種等級的問題,非常值得。
併發要靠 race detector
只要程式有並發,我的測試一定加上 -race 去跑。race detector 幫我抓過好幾個肉眼完全看不出來的資料競爭——那種「平常都對、上線高併發才偶爾算錯」的鬼故事,很多就是 race。能在 CI 自動抓出來,價值非常高。
測「失敗」比測「成功」重要
金流的可靠性不在正常流程,在異常流程。所以我會刻意製造失敗來測:
- 外部 API 逾時、回傳錯誤、回傳一半斷線
- webhook 重送、亂序、晚到
- 寫到一半程式崩潰,重啟後狀態對不對
讓假的依賴回傳各種錯誤與延遲,看你的系統會不會重複扣款、會不會卡在中間狀態、能不能自己恢復。這些測試寫起來最麻煩,但它們才是真正在保護你。
小結
我在 Go 裡測金流系統的原則:
- 不追求覆蓋率數字,火力集中在金額、狀態、冪等、併發、失敗路徑
- table-driven test 把邊界一列列攤開
- 用小介面隔離不相關依賴,讓單元測試快又穩
- 冪等、交易、唯一約束這種要用真的資料庫做整合測試
- 有並發就用 -race
- 認真測「失敗」,那才是金流真正的風險所在
測試寫得好不好,差別不在程式會不會動,而在某天凌晨對帳差一塊錢的時候,你是能很快定位、還是徹夜難眠。
留言討論
有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。