隨機測試資料真正的問題不在「隨機」兩個字,而在它無法重現。同一個用例昨天通過、今天失敗、明天又通過,而這幾天裡沒有人動過業務程式碼。這類失敗最耗時間,因為你沒有穩定的觀察窗口,沒辦法把它交給別人去查,也沒辦法在本機把那組輸入還原出來。
隨機資料在流水線裡造成哪些連鎖反應
第一層是排查成本。流水線報錯時你手上只有一筆失敗紀錄,如果它是執行當下才抽出來的,除非當時就印了出來,否則你沒有辦法把它塞回本機重跑。很多流水線的日誌只留斷言訊息、不留輸入,於是只能靠重跑碰運氣,而重跑只是重新抽一次籤。
第二層是失敗會傳染。某個用例因為隨機值剛好落到邊界組合而失敗,重跑之後又過了,團隊便習慣性地「再跑一次看看」。這個習慣一旦養成,真正的回歸失敗也會被當成抖動忽略,久了你就不再相信紅燈。
第三層更隱蔽:一批用例共用同一份隨機資料,某次執行中某個欄位偶然超出長度限制,同一個原因讓五筆用例一起失敗。於是你會先去查那五筆用例之間的關係,方向從一開始就錯了,等於在一條錯的線索上投注時間。
還有一個容易被忽略的代價:隨機會掩蓋覆蓋率。只跑一次的隨機用例說不出某個分支到底被走過沒有,在合併請求的討論裡,你也就講不清這次改動實際碰到了哪些路徑。
可重現需要同時固定哪三件事?
種子、演算法、資料版本,缺一不可。三者只要有一個在浮動,測試就還是不穩定。
| 要固定的東西 | 它決定什麼 | 浮動之後的症狀 |
|---|---|---|
| 種子 | 偽隨機序列的起點 | 每次執行拿到的紀錄都不一樣 |
| 演算法 | 產生邏輯與欄位順序 | 換版本後同一組種子給出不同結果 |
| 資料版本 | 名稱庫、行政區與郵遞區號規則 | 同一筆紀錄裡的地名悄悄改變 |
種子決定序列的起點。絕大多數語言的隨機數產生器都是偽隨機:給定同一個種子,序列完全相同,所以固定種子是讓結果穩定的最低成本手段。這也是為什麼同一支程式在不同機器上能產生一模一樣的序列。
演算法指的是產生邏輯本身。函式庫升級、權重表調整、欄位順序變化,都會讓同一個種子產出不同結果。這就是為什麼只固定種子不夠,還要固定產生器的版本,把它當成相依套件一樣寫進設定裡。
資料版本指底層的名稱庫、行政區列表與郵遞區號規則。這部分最容易變,因為它常常來自外部資料,更新一次,同一筆紀錄裡的城市名就可能換掉,而且這類更新通常不會發出通知。哪些國家和地區有資料、分別到什麼顆粒度,可以先在國家目錄裡確認,免得對著一個空的資料集除錯種子。
固定種子具體該怎麼落地
原則是種子不要在函式內部臨時產生,而是從外部傳入並記錄下來,把它當成一個普通參數看待。期望值同樣要跟著種子走,而不是手寫一份「看起來對」的資料。如果期望值是手工填的,固定種子反而會讓你誤以為問題解決了,實際上只是把隨機失敗換成了固定失敗。
第二件事是把失敗當下的輸入落盤。在流水線裡加一步「失敗就保存本次使用的種子與產生結果」,排查時直接取回本機重跑。這個動作成本很低,收益是每一次抖動都能變成可重現的缺陷。這份紀錄用建置產物來存比塞進日誌合適,因為它的用途是被取回,而不是被人閱讀。
為什麼失敗當下的輸入一定要留下來?
因為沒有它,重現就只能靠猜。日誌裡留下一句斷言失敗,能告訴你的只有「某個值不等於某個值」;留下種子與產生結果,才能讓下一個人不必先讀完整份測試程式碼。種子本身不是秘密,它只是重現所需的輸入之一,把它藏起來只會讓下一次失敗更難查。反過來說,一個只存在於某次流水線日誌裡、事後找不回來的種子,等於沒有。
怎麼把一份樣本長期釘住?
固定種子仍有一個限制:它綁定的是你這次用的產生器版本。更穩的做法是讓產生過程無狀態而且可定址,不去問「下一個隨機值是什麼」,而是問「給定這個識別碼,結果是什麼」。
本站的身份產生器就是按這個思路做的:所有工具共用一個身份 KEY,同一個 KEY 加上同一個國家與性別,永遠產出同一筆紀錄。測試裡需要穩定樣本時,把 KEY 寫進固定裝置,而不是把整筆紀錄複製下來。複製紀錄的問題是它在資料版本更新後不會跟著變,KEY 會。
批次匯出時這一點尤其有用:同一批 KEY 在本機與流水線裡得到的結果一致,斷言可以直接比對欄位,不需要先跑一遍產生器再把結果印出來看。需要換一批新樣本時,換 KEY 就好,改動範圍只有一行,比逐筆修改紀錄可靠得多。
樣本要怎麼命名、怎麼放進版控才不會腐化,可以接著讀測試固定裝置裡的身份資料。而當某一筆樣本看起來「有點怪」,通常是欄位之間的依賴被打破了,而不是單一欄位格式寫錯,這部分整理在身份資料一致性。
每個測試都該有自己的種子嗎?
不一定要,但共用一個全域種子是最常見的隱性共享狀態。多個測試平行執行時,如果它們向同一個全域產生器取值,執行順序就會影響各自拿到的序列,失敗因此變得看運氣。
實務上有兩種收斂方式。一種是讓每個測試擁有自己的種子命名空間,例如用測試名稱推導種子;另一種是把產生階段與斷言階段分離,先產生一批固定資料,再讓平行執行的測試消費這批資料。兩者都指向同一件事:不要讓測試的輸入取決於其他測試的執行時機。
還有一個常被忽略的來源是時間。不要讓產生器直接讀系統時鐘,把「目前時間」當成一個顯式參數傳進去,測試裡傳固定值,執行時才傳真實值。這一條對涉及年齡、證件有效期限、訂閱週期的欄位尤其重要,否則整批用例會在某一天突然集體失敗。
需要多樣性的測試該怎麼辦
可重現不等於必須恆定。有些測試要的正是多樣性,例如想確認系統能處理任意合法輸入,這時候固定一份樣本反而沒有意義。這類測試的做法是把隨機性限制在可控範圍內:先隨機產生,把當次使用的種子與樣本落盤,失敗時用這筆紀錄重現,而不是每次都重新抽一次。
壓力測試與模糊測試通常屬於這一類。它們不需要固定樣本,但需要留下那一次出問題的輸入。換句話說,可控的隨機遠好過失控的隨機,差別只在有沒有把那一次記錄下來。
幾個常見的錯誤做法
- 種子只固定了一部分,例如主要產生器固定了,二次排序卻讀了系統時間。
- 產生器直接讀系統時鐘,涉及年齡或有效期限的用例會在某一天集體失敗。
- 斷言依賴集合的走訪順序,同一批資料在不同執行環境得到不同結果。
- 期望值硬編碼,種子固定了卻換來一份永遠不會更新的樣本。
- 把種子藏在某一次的日誌裡,事後找不回來,等於沒有固定。
- 平行執行時共用一個全域產生器,讓輸入取決於其他測試的執行順序。
下一步
先盤點流水線裡有幾條用例會因為資料而偶發失敗,替它們各準備一個可傳入的種子,並把失敗時的輸入改成建置產物保存。接著替最重要的三條流程各釘住一份樣本,讓失敗可以穩定重現。想了解可重現樣本在測試資料整體規劃裡的位置,可以從測試身份資料是什麼讀起,也可以對照測試固定裝置裡的身份資料怎麼組織看更完整的來龍去脈。
本文談的是測試資料的產生方式與保存習慣,用來讓自動化測試的結果可以被重複觀察;文中提到的紀錄與識別碼都是為了說明流程而舉的例子,不對應任何真實人物,也不能當成任何查驗程序的依據。