程式設計面試:如何用 Go fuzz 測試把失敗輸入變成回歸語料?
題目與適用場景
你維護一個 Go 輸入解析器,線上偶爾遇到 Unicode、截斷位元組或極端長度。請說明如何用 testing.F 設計 fuzz target、哪些輸入應作為種子、如何表達無法預先知道精確輸出的性質,以及失敗後如何修復並保留回歸語料。
題目適合後端、基礎設施與測試工具職位。Microsoft 的技術面試指引明列測試、邊界與安全影響;Amazon 的 SDE 指引也把程式設計與系統基礎列為準備主題。重點是把測試不變量連到可重複的工程流程。
面試官考察點
- 能否區分範例測試的預期值與 fuzz 測試的性質。
- 能否選擇快速、無外部副作用的 fuzz target。
- 能否解釋種子語料、覆蓋率導引、失敗輸入最小化與回歸執行的關係。
- 能否處理無效 UTF-8、空輸入、超長輸入與資源上限。
- 能否把發現、修復、重跑和提交語料說成閉環。
一般回答只會說「隨機產生輸入」;強回答會給出不變量、命令、失敗檔案位置與 CI 分層策略。
回答前需要釐清的問題
- 解析器輸入是
string、[]byte或多個欄位?這會決定 fuzz 參數型別與語料格式。 - 哪些無效輸入應回傳錯誤?若允許錯誤,就不能要求所有輸入都成功。
- 單次呼叫的資源上限是多少?慢輸入可能是阻斷服務訊號。
- 目標是找 panic、語意錯誤還是相容性回歸?不同目標需要不同斷言與種子。
30 秒回答框架
我會先寫出可判定的不變量,例如「編碼後再解析得到同一結構」或「無效輸入只能回傳受控錯誤」。用 f.Add 放入真實邊界樣本,再在 f.Fuzz 中保持 target 純函式、快速且有資源上限。平時用 go test 跑種子和已保存失敗樣本,專門的 CI 任務再用 go test -fuzz 擴大覆蓋。Go 會最小化失敗輸入並寫入 testdata/fuzz/<名稱>;修復後我會重跑樣本、完整單元測試,再把語料當作回歸資產提交。
分步驟深入解答
1. 從性質而非答案開始
解析器可用往返性質:Encode(Parse(x)) 正規化後應等於 x。字串轉換可檢查冪等、長度守恆或 UTF-8 有效性。性質必須允許規格化差異,不能把偶然的位元組布局當成契約。
2. 設計可控的 fuzz target
func FuzzParseRoundTrip(f *testing.F) {
f.Add([]byte("name=alice"))
f.Add([]byte{})
f.Fuzz(func(t *testing.T, input []byte) {
if len(input) > 1<<20 { t.Skip() }
got, err := Parse(input)
if err != nil { return }
again, err := Parse(Encode(got))
if err != nil || !Equal(got, again) { t.Fatalf("round trip failed") }
})
}f.Add 的型別與順序必須和回呼參數一致。target 不應寫檔、發請求或依賴時間,否則失敗難以重現。
3. 讓種子涵蓋業務邊界
種子應涵蓋協定版本、空值、重複欄位、非 ASCII、截斷輸入與脫敏後的線上樣本。Go 會先用種子執行一般測試,因此每個種子都要便宜穩定。
4. 執行與分層
提交前執行 go test -run=FuzzParseRoundTrip 驗證種子;專門 CI 執行 go test -fuzz=FuzzParseRoundTrip -fuzztime=10s。-fuzztime 設定探索上限,並行度與資源預算由 CI 控制。
5. 讀取失敗輸入並判斷根因
Go 會把仍能觸發錯誤的輸入最小化,寫入 testdata/fuzz/FuzzParseRoundTrip/。先用 go test -run=FuzzParseRoundTrip/<id> 重播,再判斷是斷言、panic、資源耗盡或測試非確定性。
6. 修復後固化發現
修復後先重播失敗樣本,再執行完整 go test。失敗語料在不帶 -fuzz 的一般測試中也會執行,因此應檢查敏感資料、脫敏方式與大小上限。
7. 監控 fuzz 品質
覆蓋率長期不增時,增加結構化種子或調整輸入生成,不要只延長時間。若 target 常因逾時失敗,先縮小輸入、隔離昂貴路徑,並把問題當作效能缺陷處理。
高品質示範回答
我會把 fuzz 目標定義成可重複的性質,而不是期待每個隨機輸入都有業務輸出。解析器合法輸入可要求解析後再編碼仍代表同一結構;非法輸入必須回傳受控錯誤,不能 panic。先用 f.Add 放入空輸入、最大長度、重複欄位、Unicode 與脫敏樣本,再在 f.Fuzz 中限制長度並避免外部副作用。開發時用 go test -run=FuzzX 跑種子,CI 再用 -fuzztime 探索。失敗後重播最小化樣本、修復、跑一般測試與 fuzz,最後提交 testdata/fuzz/FuzzX 語料,讓隨機發現成為每次提交都執行的回歸約束。
常見錯誤
- 把 fuzz 當隨機壓力測試 → 沒有可判定性質 → 先寫不變量與錯誤契約。
- target 呼叫真實服務 → 網路、時間與狀態造成不穩定 → 改用固定 fake。
- 只在 fuzz 模式跑失敗樣本 → 修復可能回歸 → 將樣本保留在
testdata/fuzz。 - 無限制接受超長輸入 → 單一樣本吞噬時間 → 設定輸入上限並記錄跳過理由。
- 提交所有生成語料 → 儲存庫膨脹 → 只保留能重現缺陷或覆蓋關鍵分支的樣本。
追問及應對
如果解析器允許規格化,往返斷言會不會太嚴格?
會。改比較規格化後的 AST 或欄位集合,明確忽略順序、空白和大小寫差異。
失敗樣本包含使用者機密,可以直接提交嗎?
不行。先檢查憑證與個資,脫敏後重新確認仍能觸發缺陷;無法安全提交時保留受控內部重現與合成樣本。
CI 只有五分鐘,如何安排 fuzz?
每次建置執行種子與已知失敗樣本;探索任務設定固定時間、套件範圍與資源預算,逾時要記錄而非靜默忽略。
如何判斷找到的是測試 bug?
用最小樣本重播,檢查全域狀態、隨機數與 goroutine 時序,再寫確定性單元測試;不穩定時先修測試隔離。
多個 fuzz target 可以共享語料嗎?
只有輸入格式與語意契約一致時共享轉換邏輯;每個 target 仍保留自己的目錄與不變量。