判準寫不下來的時候:性質、變形關係與留給人的部分
判準的形態可以退階:算得出每個輸入的正確答案時用逐例斷言,算不出答案但寫得出答案必須滿足的條件時用不變量,連條件都寫不出來但知道輸入變化與輸出變化的對應時用變形關係。三者的共同點是判準仍然由人決定;再往下就是判準本身無法事先寫下的區域,那裡的工作交不出去。
這條退階在 agent 產出的情境下比過去更常用到,原因有兩個:產出的規模讓逐例斷言的撰寫速度成為瓶頸,而逐例的預期值又是最容易被實作污染的一種——跑一次實作就能把答案抄進斷言。不變量與變形關係比較不容易被抄:它們沒有現成的形態可以複製,要從實作反推得先看出一條性質,那是一個刻意的動作而非順手的動作。這是機率的差別不是構造上的不可能——從實作觀察出一條對應再寫成斷言仍然辦得到,而那正是本章末段要處理的失效模式。
三層退階的選用
| 判準形態 | 需要事先知道 | 適用時機 | 對出處污染的抵抗力 |
|---|---|---|---|
| 逐例斷言 | 每個輸入對應的正確輸出 | 規格明確、答案算得出來 | 低——可從實作抄回 |
| 不變量 | 所有輸出都該滿足的條件 | 答案算不出來、條件寫得出來 | 高——必須從需求推導 |
| 變形關係 | 輸入變化與輸出變化的對應 | 條件也寫不出來、只知相對關係 | 高——同樣要從需求推導、實作裡沒有現成形態可抄 |
選幾層看兩件事——答案算不算得出來決定上界(算得出來就從逐例開始),錯了的代價決定下界(代價高、或這段邏輯會被重試與併發碰到,就一路補到變形關係)。選用不是三選一,同一個功能常常三層都有:折扣計算的幾個關鍵金額用逐例斷言釘死,「折扣後金額不超過原價且不小於零」是不變量,「數量加倍時折扣後總額不會少於原本」是變形關係。三者各自抓不同的錯,而後兩者在需求被誤讀時仍然會變紅。
不變量怎麼寫得有鑑別力
性質太弱是這類測試主要的失效方式。「結果不會是 null」對所有實作都成立,包含錯的那些。判斷方式與突變測試一致——問這條性質擋不擋得住某個具體的錯誤寫法。「想出一個具體的錯誤寫法」這一步交不出去,可執行的只有「想不出來就代表性質太弱」那半句。
下面六類在業務系統裡最常成立,前四類是骨幹、後兩類補上骨幹漏掉的形態。用法是逐類問「這個功能上它成不成立」、不成立寫一句理由——挑兩三類寫完就收工是最省力的走法,而模組的核心論證(涵蓋面不等於密度)在這裡同樣適用:六類裡沒被問過的那幾類,增量是零。
往返。 編碼後解碼得回原值。適用於序列化、加解密、格式轉換、匯出匯入。這一類的鑑別力來自它同時約束兩個方向,單邊寫錯就會被抓到。
守恆。 操作前後某個量不變或單調變化。轉帳前後總額相同、合併訂單後品項總數不變、分頁遍歷完的元素數等於總筆數。這一類特別適合抓「資料在某條路徑上被吃掉」的缺陷,而那類缺陷逐例斷言很難覆蓋到。
對照。 與一份簡單但慢的參照實作結果相同。最佳化過的演算法是最直接的用法,而業務系統裡更常見的是另外兩種:費率或稅務試算拿還在跑的舊系統當參照(遷移期間免費附贈一份參照實作),報表聚合拿一句直白的 SQL 對照程式裡那套分批累加的邏輯。划不划算看一件事——寫那份參照實作要花多久:邏輯本身十行內寫得完就值得,要重建一套領域模型才寫得出來就不值得,那時改用守恆性質。
冪等。 執行兩次與執行一次結果相同。適用於正規化、去重、部署腳本、以及所有會被重試的操作。這一類在有重試機制的系統裡是必須的,而它幾乎不會出現在逐例測試裡。
上下界。 輸出永遠落在某個範圍內——折扣後的金額不超過原價也不小於零、庫存不會是負數、百分比不超過一百。最容易寫、鑑別力也最低,但它抓的那一類缺陷(溢位、正負號、單位換算)逐例測試常常整批漏掉。
順序無關。 打亂輸入的順序,結果應該相同。分散式與批次處理裡最要緊的一類——訊息亂序抵達、分區處理順序不固定、平行聚合的合併次序,這些在單執行緒的逐例測試裡永遠不會出現。
生成器要跟著資料的真實形狀走,而「真實形狀」要靠量測取得。可執行的取法是從正式環境的資料抽一份樣本(拿不到正式資料時——新系統、或沒有存取權——退而求其次去量既有的錯誤日誌與客訴紀錄,那裡的輸入形狀最接近真實的極端),對每個欄位量四件事——長度分佈的極值與中位數、字元集(有沒有多位元組、控制字元、前後空白)、空值與缺欄的實際比例、以及重複值的集中度——再把這四項餵回生成器的參數。預設生成器產出的字串多半是短的隨機 ASCII,量過就會知道它離真實有多遠。生成器沒有涵蓋的形狀,性質再強也驗不到,這與Test data 代表性是同一個問題。
失敗時框架回報的最短反例是產出本身——它通常直接就是缺陷的最小重現案例,可以原樣固定成一個逐例測試,讓這次的具體失敗有穩定的回歸點。
變形關係適用的位置
適用訊號是「跑出來的結果對不對沒有人說得準,但如果它變成這樣就一定錯了」。推薦排序、搜尋相關度、路徑規劃、費率與稅務試算、以及任何輸出帶有不確定性的元件,都落在這一類。
關係的方向要從業務語意選,不同的關係抓不同的缺陷:
- 搜尋條件收緊時結果集應該是原本的子集——抓過濾邏輯
- 輸入清單順序打亂時結果集應該完全相同——抓對輸入順序的隱性依賴
- 所有價格同乘一個係數時總價同比例變化——抓計算裡漏掉的加項
關係要從需求推導而不是從實作觀察。跑兩次看到某個對應成立就把它寫成斷言,等於把實作現在的行為當成規格,那是出處污染的另一種形態。
歸因需要額外一步:逐例斷言變紅指向一個具體的輸出值,變形關係變紅只說明兩次執行的關係不對,缺陷可能在任一次執行裡。縮小範圍的做法是把複合關係拆成單步關係,讓每次只改變一個輸入維度。
退階的三層判準由機制推導,不依賴實驗;引用到的數字與其他三章同源,射程見模組入口。
判準寫不下來的那一區
三層退階之後仍然有寫不下來的部分。錯誤訊息會不會讓使用者以為是自己打錯、這個流程走到一半被打斷時使用者知不知道發生什麼事、這份報表的數字對不對得起業務直覺——這些的判準在觀察到當下才成形。
這一區與可自動化的部分之間有一條明確的分界:判準在執行之前寫得下來的屬於檢查,可以交給機器重複執行;判準要在觀察當下才成形的屬於探究,交不出去。同一個功能通常兩側都有——登入失敗三次鎖定帳號是檢查,鎖定之後的求助路徑通不通要有人真的走一次。
資源分配要跟著改的第一件事,是不再拿套件規模當思考量的代理指標——兩者為什麼脫鉤,testing vs checking 那張卡有完整的推導。
實際的分配方式是把省下來的人力移到檢查產生不了的地方:判準本身的設計、驗收條件的維度盤點、以及那些沒有被任何斷言涵蓋的問題。把省下來的時間拿去逐行讀機器寫的斷言,等於把人放回機器已經做得比較好的那一側。
邊界
不變量與變形關係不取代逐例斷言。關鍵的具體數值仍然需要被釘死——稅率、額度上限、狀態碼的對應,這些用性質表達會失去精度,而它們正是最常被改壞的那些值。
另一個邊界是成本:性質式測試的每次執行都跑數十到數百個案例,放進每次提交的回饋迴圈會拖慢節奏。常見的配置是提交時跑固定的少量案例、夜間排程跑完整的隨機探索,並把探索找到的反例固定成逐例測試回到快速迴圈裡。
判讀訊號
| 訊號 | 該做的事 |
|---|---|
| 同一個測試被複製多次、只有輸入數值不同 | 背後有一條沒被寫出來的性質,改寫成不變量 |
| 邊界值清單越補越長、每次線上出事就補一個 | 判準是靠回憶累積的,換成生成器加性質 |
| 預期值是從實作跑一次抄回來的 | 這條斷言沒有在驗證,改成從需求推導的性質或變形關係 |
| 「結果不會是 null」這種性質 | 鑑別力不足,問它擋不擋得住某個具體的錯誤寫法 |
| 性質全綠而真實輸入形狀與生成器差很遠 | 生成器沒涵蓋的形狀驗不到,先修生成器 |
| 有重試機制而沒有任何冪等性斷言 | 補冪等性質,這類缺陷逐例測試幾乎抓不到 |
| 套件規模大幅成長而涵蓋面指不出有什麼變化 | 規模不是思考量的代理指標,回去盤點條件的維度組合(見驗收條件的等價類) |
| 團隊時間主要花在讀機器寫的斷言 | 移到判準設計與驗收條件盤點,那是交不出去的部分 |
下一步路由
- 判準為什麼要獨立於實作 → 判準的推導來源
- 判準的顆粒度怎麼定 → 驗收條件的等價類
- 判準品質怎麼量 → 品質閘門的更替
- 術語卡 → Test Oracle、Property-Based Testing、Metamorphic Testing、Testing vs Checking
- 被測對象本身是模型、輸出不確定時的評估 → LLM Benchmarking 與評估
#testing #property-based-testing #metamorphic-testing #test-oracle #exploratory-testing