一組關卡通過,證明的是產出落在該關卡切出的等價類裡,不是它就是需求要的那一個。 這則案例的價值在於等價類的大小被實際量了出來:同一份需求、同一組驗收,八個互不相同的程式全部放行,其中包含真正的行為分岔。

本則是外部案例,來源為 Robert C. Martin 的公開實驗 repo negative-test-experiment。實驗的 policy、plan、八份逐行 notebook 與結論都在 repo 內可回溯;本頁的數字取自 2026 年 8 月的 experiment-conclusion.md(當時的 HEAD 為 faf9df0,2026-08-17)。該 repo 仍在演進——作者若補跑新一輪網格或改寫結論,本頁的列號式指認(列 1、列 4、列 5 這類)就會失聯而讀者無從察覺;repo 出現觸及 experiment-conclusion.md 的新 commit 時,要回頭核對站內所有引用該實驗數字的位置——rg "negative-test-experiment|331 到 603|97% 到 99%|85.81" content/ 是目前掃得到它們的方式。本頁是這些數字的來源,其他位置引用時應指回本頁。

觀察

實驗設計是一個 4 × 2 的網格。產品固定為 Greg Yob 的 Hunt the Wumpus,合規判準固定為一份 25 案例的主控台驗收腳本。變因一是測試紀律四選一:三法則 TDD、test-last(先寫完程式再補測試)、bundling(寫一個函式就測一個函式)、完全不寫測試;變因二是 CRAP 度量是否強制壓到低於 4。八行各自從空目錄寫起,禁止複製其他行的程式碼;每行寫完後封存該行的測試套件,再從空的用突變測試長出第二套。執行者是 AI agent,policy 檔通篇是防止抄近路的條款。

指標
驗收結果八行全部 25/0 通過,包含零單元測試的那一行
程式相異度八棵樹無任何兩棵 checksum 相同;行數 331 到 603、函式數 33 到 118
行覆蓋率test-last、bundling、三法則+CRAP 三者都落在 97% 到 99%;三法則不開 CRAP 是 outlier、停在 85.81%
零測試那一行覆蓋率 14%(form)/ 24%(line),數字全部來自載入而非斷言,驗收仍然全過
最密的測試套件206 個範例、277 個斷言,而驗收結果與零測試那一行完全一致
突變測試的產出補上運算子層的翻轉(if/if-not=/not=1/0),未發現不同的遊戲

被放行的差異裡有三類不只是實作自由度:

危險判定順序。 原作的規則是先檢查 wumpus、再 pit、再 bats。八行裡有四行照做(列 3、6、7、8);另外三行(列 1、4、5)的 notebook 逐行記載都是 bats、pit、wumpus;列 2 的 notebook 未記順序。實際存在的是兩種順序,而驗收案例從來沒有把兩個危險放進同一個房間,所以兩種都合法通過。

終止行為。 一行的主迴圈永不結束,一行的外層迴圈永不離開,一行以拋出並吞掉例外的方式結束。驗收腳本沒有斷言離開的方式。

亂數來源。 三種不同的產生器(平台內建、兩種不同係數的線性同餘),意味著 --seed 參數在八行之間不是可攜的契約——同一個種子在不同行產出不同的地圖。

判讀

  1. 等價類的大小由條件之間的交叉密度決定,不由條件數量決定。 二十五條驗收案例讀起來像把功能講完了,實際上它們只約束了「每個危險單獨出現時的反應」。判定順序住在「兩個危險同時出現」這個交叉裡,而沒有任何一條條件走進那裡。這與三層測試分層談的盲區不同——那是層與層之間的縫,這是同一層內部維度沒有相乘。

  2. 同一維度上加斷言,縮小等價類的幅度隨密度遞減;對沒有任何條件碰過的維度,增量是零。 八行的單元測試規模從零到 206 個範例,而驗收結果完全一致——判定順序住在「兩個危險同時出現」這個交叉裡,25 條驗收條件一條都沒有走進去。密度成長與涵蓋面成長是兩件事,而套件規模只反映前者。

  3. 行覆蓋率在這個網格裡鑑別力很低。 test-last、bundling、三法則+CRAP 三者都停在 97% 到 99%,而它們的程式與套件組成完全不同。被覆蓋率區分出來的只有兩端:零測試那一行的 14% / 24%,以及三法則不開 CRAP 的 85.81%(作者標為 outlier,成因是主控台迴圈只被輕度驅動)。也就是說覆蓋率分得出「有沒有測試」與「有沒有一整塊沒被驅動」,分不出「測試好不好」。

  4. 突變測試守得住斷言密度,守不住規格錯誤。 實驗記載的是突變長出來的第二套測試全部瞄準運算子層的翻轉、作者的結論是它「沒有發現一個不同的遊戲」。由此可以推出而實驗未直接測量的一件事:突變是對照著程式現在的樣子產生的,所以它不會對判定順序產生任何訊號——把 bats 排在前面的那份程式跑突變測試,長出來的斷言會把那個順序釘得更牢。這個限制在測試與實作出處不獨立時特別要緊,因為那正是規格層錯誤最容易產生的情境。

  5. 自由度與未規定的需求要分開。 等價類大本身不是缺陷——命名、模組切法、內部資料結構本來就該留給實作。八行之間的檔案切法差異屬於這一類;判定順序與終止行為不屬於,它們是需求的一部分,只是沒有被寫進條件裡。

策略

從這則案例抽出的可重用修法在 #277(設計驗收條件時問哪些不同的程式會通過、交叉優先於單維度密度、把通過的產出當成樣本、指名誰接手了閱讀原本承擔的工作、區分自由度與未規定的需求),這裡不重述。

案例層特有的只有一條:把品質指標配對到它量不到的維度。覆蓋率門檻配突變測試,突變分數再配一次規格層的人工檢查——本則的八個版本正好示範了為什麼單一指標全綠不構成放寬下一層檢查的理由。

下一步路由