判斷標準有三種形態,差別在寫測試的人事先要知道什麼,而三種形態對這件事的要求依序降低。寫測試的人算得出每個輸入該有的輸出時,判斷標準寫成逐例斷言;算不出那個輸出、但寫得出所有輸出都該滿足的條件時,判斷標準寫成不變量(怎麼寫得有鑑別力);連條件都寫不出來、只知道輸入怎麼變會讓輸出跟著怎麼變時,判斷標準寫成變形關係(適用在哪裡)。三種形態共用同一個前提:判斷標準的內容由人決定,機器負責的只有重複執行它。這個前提在一種處境下失效——判斷標準要等到有人看見結果才成形,執行之前寫不下來,那裡的驗證工作沒辦法交給機器。

不變量與變形關係在 agent 產出程式碼的情境下,比程式碼由人逐行寫的年代更常被寫測試的人用到,兩個原因各自獨立。第一個是速度:agent 一次產出的程式碼量,讓寫測試的人來不及逐例算出每個輸入該有的答案。第二個是出處——判斷標準與被測程式出自同一次推導時,測試驗的是實作跟自己一致、不是實作跟需求一致,這條線索在測試出處那張卡;而逐例的預期值是三種形態裡最容易讓寫測試的人抄到的一種,他跑一次實作,就能把實作算出來的答案直接抄進斷言。

不變量與變形關係比較不容易被這樣抄:實作裡沒有一段程式碼長得像一條性質可以直接複製,寫測試的人要從實作反推出一條,得先看出實作裡藏著哪一條——那是一個要停下來想的動作,不是順手就能做的複製。這是抄的難度差別、不是抄不到:寫測試的人仍然可能從實作觀察出一條對應再寫成斷言,而那條對應等於把實作現在的行為當成規格。這個難度差在一種情況下會消失——寫測試的人請 agent「為這份實作寫一組性質式測試」時,反推的成本同樣接近零,抵抗力回到跟逐例斷言同級。判斷標準要獨立於實作這件事,在判斷標準的推導來源那一章。

這三種形態的分法由機制推導、不依賴實驗。本模組四章共用的那份對照實驗足以支持什麼、不足以支持什麼,寫在negative-test-experiment 對照實驗。

判斷標準形態的選擇:逐例斷言、不變量、變形關係

判斷標準形態寫測試的人事先要知道的適用時機預期值能不能從實作抄回
逐例斷言每個輸入對應的正確輸出規格明確,寫測試的人算得出每個輸入該有的答案能——跑一次實作就抄得到
不變量所有輸出都該滿足的條件寫測試的人算不出答案,但寫得出所有輸出都該滿足的條件不能——實作裡沒有一段程式碼長得像一條性質
變形關係輸入變化與輸出變化的對應連條件都寫不出來,只知道輸入變化與輸出變化的對應不能——實作裡沒有一段程式碼長得像一條變形關係

寫測試的人要用哪幾種,看兩件事。答案算不算得出來決定從哪一種起步:算得出來就從逐例斷言開始。這段邏輯出錯而沒被抓到、代價有多大決定要補到哪一種:代價高、或這段邏輯會在重試與併發的路徑上被執行,就一路補到變形關係。中間那一種不是必經——答案算得出來而代價又低的功能,逐例斷言寫完就可以停。

判「答案算得出來」時要多問一句:那個答案從哪裡來。來源是規格、需求文件或業務方說得出口的規則時,逐例斷言成立;來源是 agent 產出的那份程式碼本身(沒有規格文件、只好去讀實作),寫下的逐例斷言就是抄回來的,起步的位置要往下移一層。

三種形態不是三選一,同一個功能常常三種都有。以折扣計算為例,寫測試的人用逐例斷言寫出幾個關鍵金額的確切數值,用不變量寫「折扣後金額不超過原價且不小於零」,用變形關係寫「數量加倍時折扣後總額不會少於原本」。三者各自抓不同的錯,而不變量與變形關係在實作誤讀需求時仍然會讓測試變紅——它們的內容從需求推導,不跟著實作的理解走。

這一節的出口:答案算得出來、而且來源不是那份實作的功能,寫測試的人先寫逐例斷言,再往下讀不變量那一節決定要不要補第二層;算不出來的直接從那一節開始。三種都用上仍然驗不到的部分,在執行前寫不出判斷標準的探究工作。

不變量的偵測能力與常見的性質類別

不變量是一條對所有合法輸入都該成立的性質——寫測試的人算不出某個輸入該有的答案,但寫得出一條規則:答案不管是什麼,都不能違反它。工具側由性質式測試框架用生成器產生大量輸入逐一驗它,失敗時把觸發失敗的那個輸入收縮成最短的一個。完整的定義與框架行為在性質式測試那張卡,這一節處理的是它交給寫測試的人的那半件事:這條性質要寫到多強才有用。

偵測能力指的就是這個強度——一條性質能讓多少種錯誤的實作變紅。「結果不會是 null」對所有實作都成立,包含錯的那些,所以它的偵測能力是零。判斷方式與突變測試一致:寫測試的人要舉得出一個具體的錯誤寫法,並說出這條性質在那個寫法下會變紅(變紅指的是測試失敗)。舉出那個錯誤寫法這一步沒辦法交給機器;能機械執行的是反向的那條判定——舉不出任何一個會讓這條性質變紅的錯誤寫法,這條性質就太弱,要往下收緊。以一個回傳清單的轉換函式為例,「結果不會是 null」收緊成「結果的長度等於輸入的長度、且每個元素都來自輸入」,偵測能力才開始出現。

下面這幾類性質在業務系統裡常見。往返、守恆、對照、冪等約束的是單一操作內部;上下界與順序無關約束的是前四類碰不到的兩件事——輸出的值域,以及輸入的次序。寫測試的人的用法是逐類問「這一類性質在這個功能上成不成立」,判定不成立的就在測試檔或設計筆記裡寫一句理由;判定成立的還要再過一次上面那道偵測能力的關,否則會寫下一條恆綠的斷言卻記成已覆蓋。逐類問過與完全不問,結論都可以是「這一類不成立、不必補」,而完全不問省力得多——驗收條件的等價類那一章的判斷標準在這裡同樣適用:同一類上多寫幾條性質,收益隨密度遞減,而完全沒被問過的那幾類,補上去的收益最大。資源不足而只挑兩類做時,資源不足時測試作法的降級順序:判斷標準與實作分開產出、維度交叉盤點、性質式測試、突變測試給了一個授權的降級版本(只做往返與冪等兩類),用它之前要寫下哪一項前置不成立、用它之後要訂一個回看的時點。

往返(單一操作內部)。 編碼後解碼得回原值。寫測試的人在序列化、加解密、可逆的格式轉換、匯出匯入上用它。它的偵測能力來自同時約束編碼與解碼兩邊:任一邊寫錯,往返之後的值就對不回原值。有損的轉換不適用——地址正規化、字串截斷、精度捨去這些操作把多種輸入映到同一個輸出,沒有反函數,照往返寫下的斷言不可能通過。判別方式是問這個操作的輸出能不能唯一決定輸入。

守恆(單一操作內部)。 操作前後某個量不變或單調變化。狀態轉換型的例子是轉帳前後總額相同、合併訂單後品項總數不變、分頁遍歷完的元素數等於總筆數;純函式沒有「操作前後」,它的守恆形態是輸入單調變化時輸出跟著單調變化(重量增加時運費不會變少),也可以是某個語意欄位在轉換前後不變(地址正規化前後行政區代碼相同)。這一類特別適合抓資料在某條處理路徑上遺失的缺陷,而那類缺陷逐例斷言很難覆蓋到。

對照(單一操作內部)。 被測實作的輸出,與一份簡單但慢的參照實作的輸出相同。最佳化過的演算法是最直接的用法,而業務系統裡更常見的是另外兩種:費率或稅務試算拿還在跑的舊系統當參照——遷移期間新舊系統並行,舊系統就是一份現成的參照實作;報表聚合拿一句直白的 SQL 對照程式裡那套分批累加的邏輯。參照實作的邏輯十行內寫得完,這筆成本就划得來;要先重建一套領域模型才寫得出來就划不來,那時寫測試的人改用守恆性質。這一類有一個承重的前提:參照實作本身是對的。 新程式如果就是為了修舊程式的錯而寫,拿舊系統當參照會把舊錯誤一併固定下來;新舊不一致時該信誰,要回到需求判,不能預設舊的那份是規格。

冪等(單一操作內部)。 同一個操作執行兩次的結果,與執行一次的結果相同。寫測試的人在正規化、去重、部署腳本、以及所有會被重試的操作上用它。有重試機制的系統一定要有一條這樣的性質,而它很少出現在逐例測試裡。輸出帶隨機性或個人化時間窗的功能不適用——同一輸入兩次結果不同不是缺陷,這一類用冪等判不出結果。

上下界(值域)。 輸出永遠落在某個範圍內——折扣後的金額不超過原價也不小於零、庫存不會是負數、百分比不超過一百。這一類最容易寫,偵測能力也最低,但它抓的那一類缺陷(溢位、正負號、單位換算)逐例測試容易整批漏掉。

順序無關(輸入次序)。 寫測試的人打亂輸入的順序之後,結果應該與未打亂時相同。分散式與批次處理裡最要緊的一類——訊息亂序抵達、分區的處理順序不固定、平行聚合的合併次序每次不同,這三種情況在單執行緒的逐例測試裡不會出現。這一類判成不成立有時是正確的:重複 key 後寫覆蓋是刻意設計,那時順序就該影響結果。

生成器要跟著資料的真實形狀走,而那個真實形狀要靠寫測試的人實際量測取得。字串與結構化欄位要記下的是長度分佈的極值與中位數、實際出現的字元集(有沒有多位元組、控制字元、前後空白)、空值與缺欄的比例、重複值的集中度;數值欄位換成值域的極值與中位數、負值與零出現的比例、以及單位(公克還是公斤、含稅還是未稅)。記到的值填進生成器的參數。取樣來源依序有三條:正式環境的資料最好;拿不到時(系統還沒上線,或沒有存取權)改量既有的錯誤日誌與客訴紀錄,那裡的輸入形狀最接近真實世界會出現的極端;兩者都沒有時只剩手上的範例檔,而範例檔通常是 happy path,用它要知道形狀會偏樂觀。生成器沒有涵蓋的形狀,性質再強也驗不到,這與 Test data 代表性是同一個問題。

這一節的出口:寫測試的人照這幾類逐一問過被測功能一次,成立的寫成性質、不成立的寫一句理由;判成立的每一條再問一次舉不舉得出會讓它變紅的錯誤寫法,舉不出來就收緊。性質失敗時框架給的最短反例可以原樣固定成一個回歸測試——前提是先確認收縮之後的那個反例觸發的還是同一條路徑,shrinking 有時會縮到失去原本的語意。

變形關係的適用時機與關係種類

變形關係是三種形態裡對寫測試的人要求最少的一種:不必算出輸出,也不必寫得出所有輸出都該滿足的條件,只要寫得出輸入怎麼變會讓輸出跟著怎麼變。適用訊號是「跑出來的結果對不對沒有人說得準,但收窄搜尋條件之後結果反而變多,就一定錯了」這種句子——正確答案寫不出來,哪一種變化必然是錯的卻寫得出來。推薦排序、搜尋相關度、路徑規劃、費率與稅務試算、以及任何輸出帶有不確定性的功能,都落在這一類。

要挑哪一條關係、關係往哪個方向成立,都由被測功能的業務語意決定,不同的關係抓不同的缺陷:

  • 集合關係:搜尋條件收緊時,結果集應該是收緊前那個結果集的子集——抓過濾條件沒有實際生效的缺陷
  • 置換不變:輸入清單順序打亂時,結果集應該與打亂前完全相同——抓實作對輸入順序的隱性依賴。這一條與順序無關那一類不變量講的是同一件事,兩種形態都寫得出來:算得出結果時寫成不變量,算不出來時寫成變形關係
  • 同比例變化:所有價格同乘一個係數時總價同比例變化——抓計算裡漏掉的加項
  • 排序關係:把某一項的權重調高時,它在結果裡的名次不該往後掉——抓排序邏輯本身的缺陷,而前三種關係都碰不到這件事
  • 等價輸入:加一個無關的空白、換全形半形寫法,輸出應該相同——抓正規化沒有走完的缺陷

關係要從需求推導而不是從實作觀察。寫測試的人用兩組輸入跑一次實作,看到輸入的變化與輸出的變化之間有某個對應、就把那個對應寫成斷言,等於把實作現在的行為當成規格——那跟直接把實作跑出來的預期值抄進斷言一樣,判斷標準都是從實作推導來的。

測試變紅之後要判斷缺陷在哪,變形關係比逐例斷言多一步:逐例斷言變紅時指向一個具體的輸出值,而變形關係變紅只說明變形前那次執行與變形後那次執行之間的關係不對,缺陷可能在任一次執行裡。寫測試的人縮小範圍的做法是把一次改動多個輸入維度的複合變形關係,拆成每次只改一個維度的單一變形關係,這樣變紅時對得上是哪一個維度造成的。

輸出帶隨機性的功能還有一個前置要處理:性質式測試在那裡容易變成 flaky,而 flaky 會讓品質閘門的更替那一章的突變分數變成往上飄的噪音。寫測試的人要先把隨機來源固定下來(給定 seed、凍結時間窗),或把這組測試隔離出去,再談變形關係。

這一節的出口:對輸出不確定的功能,寫測試的人先寫出一句「輸入這樣改之後,結果往那個方向動就一定錯了」,再回頭確認那句話是從需求推出來的、不是從跑過的結果看出來的;動手之前先確認這組測試跑起來是確定性的。

執行前寫不出判斷標準的探究工作

逐例斷言、不變量與變形關係都用上之後,仍然有判斷標準寫不下來的部分。一個錯誤訊息會不會讓使用者以為是自己打錯;一個流程走到一半被網路或當機打斷時,使用者知不知道發生了什麼事;一份報表的數字符不符合業務人員對這個數量級的直覺——這幾件事的判斷標準,要等有人實際看到畫面與數字的當下才成形。

這一區與可自動化的部分之間有一條明確的分界:判斷標準在執行之前寫得下來的屬於檢查,可以交給機器重複執行;判斷標準要在觀察當下才成形的屬於探究,沒辦法交給機器。同一個功能通常兩側都有——「登入失敗三次是否鎖定帳號」這條判斷標準屬於檢查,而鎖定之後的求助路徑通不通,要有人實際走一次。

團隊要照這條分界重新分配人力,第一件事是換掉「測試套件有多大」這個指標。過去測試的數量受限於有人願意寫多少,所以套件規模跟團隊投入了多少探究大致同步成長;斷言可以被整批產生之後,兩者脫鉤——套件長大只說明產生了更多檢查,不說明有人多想清楚了什麼。

機器接手檢查之後空出來的人力,要移到檢查回答不了的問題上:判斷標準本身的設計、驗收條件的維度盤點、以及去問那些沒有被任何斷言涵蓋的問題。反過來,團隊把空出來的時間拿去逐行讀機器寫的斷言,是把人調回檢查那一側——而檢查正是機器已經做得比人快也比人齊的那一件事,人在那裡的邊際貢獻最低。

這一節的出口:團隊把被測功能的驗證項目分成兩堆——判斷標準現在就寫得下來的歸檢查、要有人看見結果才判得了的歸探究——再看這個月的時間實際落在哪一堆。

不變量與變形關係的邊界:逐例斷言的保留與執行成本

不變量與變形關係不取代逐例斷言。寫測試的人仍然要用逐例斷言把關鍵的具體數值釘住——稅率、額度上限、狀態碼的對應。這些值改用性質表達會失去精度,而它們正是 agent 改動周邊程式時最常一併改壞的那些值。

另一個邊界是成本:性質式測試的每次執行都跑數十到數百個案例(多數框架的預設值落在這個量級,可以調),團隊把它放進每次提交都要跑的回饋迴圈,會拖慢提交的節奏。常見的配置是團隊在提交時跑固定的少量案例、用夜間排程跑完整的隨機案例,再把探索找到的反例固定成逐例測試,放回每次提交的那個迴圈裡。

判斷標準形態與人力分配的判讀訊號與處置

訊號該做的事
同一個測試被複製多次、只有輸入數值不同那幾個測試背後有一條沒被寫出來的性質;寫測試的人把它們的共同斷言抽出來,改寫成一條不變量
邊界值清單越補越長、每次線上出事就補一個這份清單是靠出事的回憶累積的;寫測試的人把它換成生成器加一條涵蓋那些邊界值的性質
預期值是從實作跑一次抄回來的這條斷言只是把實作的行為重寫一次,什麼都沒驗到;寫測試的人回到需求,改寫成從需求推導的性質或變形關係
「結果不會是 null」這種性質偵測能力不足;寫測試的人舉一個具體的錯誤寫法,問這條性質會不會因為它而變紅,舉不出來就往下收緊
真實輸入的形狀與生成器產出的形狀差很遠而性質全綠生成器沒涵蓋的形狀驗不到;寫測試的人先去量正式環境的欄位形狀,把量到的值填進生成器的參數
有重試機制而沒有任何冪等性斷言重試造成的重複執行缺陷,逐例測試很難抓到;寫測試的人補一條「同一個操作執行兩次與執行一次結果相同」
套件規模大幅成長,而沒有人指得出涵蓋面多了什麼規模說明不了團隊投入了多少探究;團隊回去盤點條件的維度組合(見驗收條件的等價類)
團隊時間主要花在讀機器寫的斷言讀斷言正是機器已經做得比人齊的一件事;團隊把時間移到判斷標準設計與驗收條件盤點,那兩件事沒辦法交給機器

延伸閱讀:判斷標準的推導來源、驗收條件的等價類、突變分數、相關術語卡與模型輸出的評估