測試設計
案例決定套件能捕捉什麼。 之後的一切——測試框架、執行器、計數——只是執行那個決定。
這個技能取代的時刻
| 正要… | 改為… |
|---|---|
| 只寫快樂路徑就停止 | 遍歷輸入空間:針對每個參數,列出其類別與邊界 |
| 從實作推導案例 | 從規格推導,讓實作給你驚喜 |
| 用一個舒適的值測試 | 測試每個邊界內的最後一個值與邊界外的第一個值 |
| 只測試規格提到的邊界 | 每個範圍都有兩端;句子遺漏的那端仍然是邊界 |
| 枚舉規格列出的角色 | 加上呼叫者遺漏的:無角色、未認證、未指派 |
| 從快樂路徑變化一個條件 | 建立決策表,涵蓋兩個條件同時失敗的規則 |
| 畫出狀態表就繼續 | 在每一行放測試 ID;「不允許」這行最需要測試 |
| 將「拒絕,或接受但前提是‥‥」寫成預期結果 | 每個案例只有一個結果;無法決定的規格是開放問題,不是測試 |
| 跳過不太可能的錯誤路徑 | 每個失敗模式都是一個類別:無效輸入、資源缺失、拒絕、逾時、半完成 |
| 留下未實作的路徑,沒有測試、跳過標記或無人斷言的樁 | 寫一個測試,確認該路徑目前被拒絕為未實作;它現在通過,當有人實作那天變紅(見下方) |
| 只測試功能的主要流程 | 從使用案例讀出替代與例外流程;每個都是一個類別 |
| 透過閱讀來評定套件是否充分 | 對照規格計數:類別、兩端邊界、規則、轉換 |
| 在已涵蓋的類別中增加第五個測試 | 把這測試花在未涵蓋的類別上——第五個測試買到的是執行時間,而非涵蓋率 |
攻擊規格,而非實作
透過閱讀程式碼寫出的測試,斷言的是程式碼的行為。 它一出現就通過,將當前的錯誤編碼為期望, 並且在變更後失敗時只說「程式碼變了」, 這本來就是已知的。 這不是猜測,而是可測量的:在 24 個 Java 儲存庫中, LLM 生成的 oracle 傾向於捕捉程式的實際行為,而非預期行為 (Konstantinou, Degiovanni, Papadakis, arXiv:2410.21136)。
從東西應該做的事推導——規格、 線路格式、合約——然後才執行。 唯一的正當例外是特意鎖定當前行為的特性描述測試, 並且必須標記清楚,以免日後有人將鎖定的錯誤誤認為需求。
一個有用的氣味:如果每個測試在第一次執行時都是綠燈, 那麼無論意圖為何,該套件都是從實作推導出來的。
等價類別:計算類別,而非測試
程式碼無法區分的輸入形成一個類別, 每個類別一個測試就夠了——第二到第五個測試花費執行時間,卻沒有證明任何新東西。 這個規則雙向適用:沒有類別未被涵蓋, 也沒有類別被意外涵蓋兩次。
一個有十七個測試的套件看起來很徹底。 但它是否徹底取決於這些測試落在多少個類別中, 而這個數字不會出現在任何執行器的輸出中。 必須手動針對輸入空間計數——而這就是測試計畫要做的事。
規格列出的類別是簡單的一半。 一個有三行的角色表有第四個類別, 即不符合任何一行的請求, 而這是從表格建立的套件永遠不會包含的。 對於每個參數,在撰寫任何案例之前,先寫出無效類別與有效類別並列: 錯誤類型、缺失、超出所有列舉值、 完全沒有權限的呼叫者。
課程大綱將此列為規則而非習慣: 在等價分割中,涵蓋項目是分割區, 100% 表示每個識別出的分割區都被執行,「包括無效分割區」, 且分割區「不得重疊,且必須是非空集合」 (ISTQB CTFL v4.0.1 §4.2.1)。 對於多個參數,它命名了最弱的標準 Each Choice——每個參數的每個分割區各一次, 忽略組合——這是大多數套件在不知其名時所達到的程度, 也是下方決策表的起點。
邊界,因為那裡是缺陷所在
差一錯誤是最常見的算術缺陷,
並且除了在邊界處,其他地方都看不見。
每個邊界上重要的配對是邊界值及其跨越線條的最近鄰居,
這就是 2-value BVA (CTFL v4.0.1 §4.2.2);
3-value BVA 還加上內側的鄰居,
課程大綱展示了這帶來了什麼:
一個原本應為 x ≤ 10 但寫成 x = 10 的檢查,在 10 和 11 都會通過,
只有 9 能抓住它。
常見的邊界:空、恰好一個、多個;零和負數;
宣告的最大值及其後一個值;
恰好等於緩衝區、限制、逾時的值。
來自此機器的一個具體配對: 一個在 8,190 個字元時正常,但在 8,192 個字元時必須失敗的 PATH 檢查, 必須在這兩個值上測試。 在 4,000 和 12,000 通過,對於介於兩者之間的界線幾乎無法證明什麼。
當邊界是陳述的前置條件時,
超出邊界值是呼叫者的錯誤,而不是具有預期結果的案例;
design-by-contract 決定給定的邊界屬於兩者中的哪一個,
測試隨後斷言適用的那個。
規格未命名的另一側
「最多 10 MB」命名了範圍的一端。 另一端是否存在,無論句子是否提及, 從文字推導的案例在文字停止處停止: 一個測試了上邊界在 −1、0 和 +1 位元組,但沒有測試 1,024 以下任何值的套件, 只對半個範圍進行了邊界分析。 定義沒有偏向任何一側: 「一個分割區的最小值和最大值是其邊界值」 (CTFL v4.0.1 §4.2.2)。 首先為每個分割區寫下兩端, 然後將每個端點與相鄰分割區中的最近鄰居配對。 未命名的那一側通常是零、空或一, 而規格通常對此保持沉默,因為沒有人決定。
同時移動兩個條件
透過從快樂路徑改變一個條件建立的套件,涵蓋了每個條件的每個值,但幾乎沒有涵蓋它們的組合: 格式錯誤、大小超標、數量超標,每個都單獨出現, 而沒有兩個同時失敗的案例。 當兩者同時適用時,哪個錯誤優先,通常也不在規格中, 因此缺少的案例隱藏了一個缺失的決定。
將條件佈置為決策表並計算規則,
然後涵蓋兩個條件同時失敗的規則,以及角色限制和格式限制同時適用的規則。
在決策表測試中,涵蓋項目是包含可行組合的欄位,
100% 表示每個這樣的欄位都被執行 (CTFL v4.0.1 §4.2.3);
課程大綱補充說,該表「有助於發現需求中的任何缺口或矛盾」,
這就是上面提到的缺失決定自行浮現的情況。
同一份文件在 Each Choice 下可能被視為完全涵蓋,但在欄位下可能只有四分之一被涵蓋,
只有第二個數字能說明組合的情況。
當規則數量是多個旗標的乘積時,
更便宜的解決方案在上游:
state-first 將旗標折疊為可能發生的狀態,
表格便縮減為那些狀態。
表格是關於套件的聲明
狀態表、決策表、使用案例的流程清單: 每一行聲稱有一個測試存在於該行。 在行中放入測試 ID。 沒有 ID 的行是未涵蓋的類別, 而寫著「不允許」的那行最可能是空的, 因為寫出禁止事項感覺就像已經處理了它。 禁止的轉換——在一個傳送正在進行時進行第二次傳送——就是測試, 而應該阻止它的守衛就是被測試的對象。
狀態表使該行明確: 無效的轉換是一個空的儲存格, 而課程大綱定義的三個標準中最強的一個, 所有轉換覆蓋率, 要求嘗試每一個轉換——每個測試案例一個, 這樣一個缺陷就不會掩蓋另一個 (CTFL v4.0.1 §4.2.4)。 有效轉換覆蓋率,大多數套件停止的 0-switch, 從不觸及那些儲存格, 這就是為什麼未經測試的「不允許」行仍然保持綠色。
然後將兩份文件相互對照。 一個測試的路徑與表格不一致(表格說 failed → sending,測試是 failed → selected → sending)表示其中之一是錯的, 而除了比較之外,沒有其他方法能找出哪個。 自我轉換以及從失敗狀態出發的轉換也計為行。
每個案例一個結果
「拒絕,或者如果接受,則在傳送後不留下損壞的資料」無法通過或失敗。 它誠實地出現,當規格從未說明零位元組檔案會發生什麼事,且作者拒絕發明答案時, 它之所以能通過審查,是因為案例明顯存在。 這個析取隱藏在預期欄位中。 正確的形式是向擁有規格的人提出問題, 如實列出,且在得到答案之前,不為該類別撰寫測試。
錯誤路徑也是類別
不快樂的路徑是未經測試的程式碼積累的地方, 因為正常使用中沒有什麼會執行它們——鎖持有者中途死亡、 不存在的映射資料夾、 缺少預期欄位的輸入。 每個都是一個等價類別,與任何輸入範圍一樣有權利要求一個測試。 一個只證明工具在一切順利時正常運作的套件,測試的是展示, 而不是工具。
使用案例在程式碼存在之前枚舉類別
使用案例是一個已經切割成類別的規格:一個主要流程、 每個替代流程、每個例外流程、 以及可以進入每個流程的前置條件。 無論誰寫的,都已經做了這個技能要求的枚舉——替代和例外流程就是錯誤路徑——因此一個執行主要流程的套件只涵蓋了文件所列 N 個類別中的一個。
從使用案例讀出流程,並為每個流程安排測試, 然後檢查前置條件, 因為一個流程只有在其前置條件可以成立時才能到達。 一個翻譯頁面的奇偶檢查有一個主要流程(兩種語言都被編輯和提交)、 一個替代流程(一個單邊編輯宣告為例外)、 以及兩個例外流程,它們只有在前置條件被寫下時才會出現: 樹是髒的,所以還無法判斷是否過時; 歷史被改寫,所以宣告的提交不再可解析。 兩者在一個網站上都在一週內被觸發。 只有快樂路徑的話,兩者都不會顯示。
一個例外流程是否真的會發生,不是由它感覺有多不可能來決定。
操作範圍說明了什麼超出範圍,危險分析說明了還有什麼;
兩者都存在於 assurance-case 中,
而這個技能採用它們產生的清單,而不是憑直覺修剪它。
覆蓋率是在命名標準下的一個數字
同一份測試文件用三種方式測量:100% 的條件、90.9% MC/DC、
25% 的決策規則。
三者對於同一個檔案都是真實的,
因此沒有標準的覆蓋率數字什麼也沒說,
而在計數後才選擇標準的數字則說得更少。
在計數前就命名它,
使用課程大綱自身的術語,以便數字可以被檢查:
分割區覆蓋率和 Each Choice (§4.2.1)、
2-value 或 3-value BVA (§4.2.2)、決策表欄位 (§4.2.3)、
所有狀態、有效轉換或所有轉換 (§4.2.4)。
MC/DC 不在基礎課程大綱中;
它來自安全標準 (DO-178C, ISO 26262),
將其與其他標準並列而不說明,是一種偽裝成嚴謹的混合引用。
一個數字還欠讀者什麼,在 measured-claims 中。
檢查套件:計數,而非判斷
詢問套件是否充分,會得到產生它的盲點。
在同一個模型上進行第二次會話會共享這些盲點,
同一個人在一天後也會。
課程大綱稱之為「某種獨立性」,
其四個層級中的第二個,並說明了價值從何而來:
「作者的認知偏見與測試者的認知偏見之間的差異」 (CTFL v4.0.1 §1.5.3)。
在同一個模型上進行第二次會話會共享偏見,只保留標籤。
不共享偏見的步驟是計數:
從規格列出類別並勾選有測試的類別、
列出每個範圍的兩端邊界、列出決策表的規則、
列出每個狀態表的每一行、
並引用每個空白的檔案和行號。
預期值是否正確仍然是一個判斷,並留給沒有撰寫它的讀者;
一個類別是否有測試則是算術。
如果計數是抽樣而非窮舉,
報告應說明是抽樣。
用於判決的預設反駁形式在 adversarial-verify 中;
這個技能提供要計數的內容。
當需求移除一個類別
「雙重傳送不可能發生,因為按鈕被停用,刪除那一行」憑藉一個層級中的一個守衛,從套件中移除了一個類別。 照要求做,並用一句話說明哪個類別現在未涵蓋,以及什麼會到達它: 狀態機仍然接受來自任何不是按鈕的呼叫者的第二次傳送。 附帶那句話刪除的行是一個決定; 沒有那句話刪除的行是一個再也找不到的缺口。
未實作的路徑有一個測試來表明它
刻意留在此次變更之外的路徑、
此版本不支援的角色、尚未解析的格式、
拋出 NotImplementedException 或呼叫 todo!() 的分支,
仍然是輸入的一個類別,
而套件記錄了今天在那裡發生的事情。
撰寫測試,確認該路徑被拒絕為未實作:
它引發的例外或錯誤值、
缺失的功能表項目、回答「不支援」的請求。
那個測試現在通過,並完成兩項工作。 它將缺口放在套件的清單中, 讓計算類別的讀者能找到它, 而不是放在註解或工單中。 並且當有人實作該路徑的那一天,它會變紅, 這正是必須撰寫其真實案例的時刻; 失敗的測試是提醒,並且它被取代,而非刪除。
跳過標記或待定測試不是測試;