Test Effectiveness
判斷「這套測試是否真的在驗東西」。職責:識別覆蓋不真實的形態、提供證明覆蓋真實的手段。
核心主張
綠燈的資訊量比它看起來少。
測試通過只證明「在被執行的路徑上,斷言成立」。它不證明被測對象被執行過、不證明斷言被執行過、不證明斷言在驗有意義的行為、也不證明失效時會變紅。
劃分軸:綠燈在哪個環節失去資訊量
一次有效的測試要走完四個環節:被測對象被執行、斷言被執行、斷言驗對對象、斷言有區別力。任一環節斷掉,綠燈即不再攜帶程式碼正確的資訊。第五族的斷點不在鏈上,而在「綠燈的來源不是程式碼」。
| 斷點 | 族 |
|---|---|
| 被測對象未被執行 | 覆蓋歸零族 |
| 斷言未被執行 | 斷言未執行族 |
| 斷言驗錯對象 | 覆蓋錯位族 |
| 斷言區別力不足 | 否定結果雙義族 |
| 綠燈來源非程式碼 | 環境依附族 |
此軸是本 skill 的分類依據,也是它可被證偽的方式:遇到落不進五族的形態,是軸不完整而非該形態不存在。
覆蓋歸零族:被測對象未被執行
判準:
把被測對象換成恆回傳固定值的空殼,斷言是否仍綠?仍綠即有效覆蓋為零。
此問法不依賴替身存在——測試不使用替身時同樣可操作。替身情境是其特例:移除所有替身後若剩下的斷言只在驗標準函式庫行為,等同被測對象從未參與。
| 形態 | 識別訊號 | 為何危險 |
|---|---|---|
| 替身掉唯一有風險的函式 | 被測模組中唯一與環境耦合的函式被替身取代 | 剩餘斷言只在驗標準函式庫,被測邏輯零覆蓋 |
| 守護死碼 | 被測函式在生產路徑無呼叫者 | 測試數量計入覆蓋,實際保護不會執行的程式碼 |
| 空跑綠燈 | 對集合迭代斷言,集合為空時無斷言執行 | 被測機制完全失效(回傳空集合)時測試反而綠 |
斷言未執行族:斷言根本沒跑到
前一族的斷言完好但沒碰到被測對象;本族的被測對象可能跑了,但斷言本身從未執行。兩者的綠燈都不含資訊,斷點不同。
判準:
刻意讓斷言失敗一次(改斷言的預期值),測試是否變紅?不變紅即屬本族。
| 形態 | 識別訊號 | 為何危險 |
|---|---|---|
| 測試未被收集 | 被 skip/tag 排除/檔名不符 runner 的收集樣式/xit 殘留 |
計入「測試數」但從未執行,覆蓋率報告與實際脫節 |
| 非同步未等待 | async 測試未 await,斷言在測試判定結束後才失敗 | runner 記綠,失敗訊息出現在下一個測試或完全丟失 |
| 斷言在未執行的路徑內 | 斷言寫在回呼、事件處理器、或被 try-catch 包住 | 回呼未被觸發、例外被吞噬,斷言靜默跳過 |
本族是實務上最高頻的假綠燈來源,且前一族的判準對它一律誤判——把被測對象換成空殼,斷言仍然沒跑,測試仍然綠,看起來像「有效覆蓋為零」以外的正常狀態。故本族的判準必須先跑。
覆蓋錯位族:斷言驗錯對象
判準:
構造一個輸入,使被測邏輯錯誤而此測試仍綠。構造得出即屬本族。
不用「答不答得出回歸行為」判定——那是能力測驗,兩個人會給相反結論,且經驗多的那位永遠判「不屬本族」。
| 形態 | 識別訊號 | 為何危險 |
|---|---|---|
| 測 bug report 而非機制 | 修復加了 N 條路徑,測試只覆蓋已知反例走的那一條 | 其餘 N-1 條無護欄,同一缺陷換個觸發向量即復發 |
| 把設計缺陷固化成規格 | 測試斷言「因為某個不該有的限制所以不作用」 | 日後修掉該限制時測試會紅,反過來阻止修復 |
| 名實不符 | 測試名宣稱驗證 X(不呼叫某函式、提前返回),斷言只檢查回傳值 | 比沒有測試更糟——讓維護者相信該機制已有護欄 |
否定結果雙義族:斷言區別力不足
**「沒輸出」「沒告警」「條件不同」同時符合「正確」與「壞掉」。**只做否定測試無法區分兩者。
| 只有否定測試時 | 必須補的正向對照 |
|---|---|
| 空目錄不告警 | 放入探針後必須告警(證明還找得到目標) |
| 兩個識別符比對後不相等 | 端對端建立目標後必須命中(證明分支可達) |
| 誤報已消失 | 真實命中仍必須告警(證明沒有矯枉過正) |
修誤報時,反向案例應多於正向:誤報消失容易確認,「真實命中還在不在」才是會被漏掉的一半。若只測前者,連「直接刪掉整個偵測器」都會通過。
環境依附族:綠燈來源非程式碼
前四族的斷點在「測試到被測對象」這條鏈上;本族的鏈是通的,但綠燈反映的是本機狀態恰好符合預期,不是程式碼正確。
| 形態 | 識別訊號 | 本機綠燈為何不構成證據 |
|---|---|---|
| 寫死專案識別符 | 框架資產的測試中出現具體專案名稱或路徑片段 | 綠燈只證明本機的專案名稱恰好符合,未驗證任何程式碼行為 |
| 前提檢查用斷言 | 用斷言檢查測試前提,使「前提不成立」轉為 failure 而非 skip | 綠燈只證明前提成立,被測行為是否正確從未被檢查 |
| 定時炸彈 | 測試依賴的狀態,正是某張進行中的任務要消滅的狀態 | 綠燈依賴一個正被主動消滅的狀態,其保護期有上限 |
檢查手段:把環境錨點指向空目錄或臨時目錄重跑。錨點依形態而異——寫死識別符對應專案根與 repo 名,前提檢查對應該前提所依賴的外部服務或檔案,定時炸彈對應被依賴的那份狀態。全通過才算不依附本機狀態。
驗證手段:變異測試
改一行程式碼,看測試會不會紅。這是目前唯一能把「測試看守了某個分支」從推論變成觀測的手段。
目標行由測試宣稱決定,不由直覺決定。 步驟 1 若停在「選一個看起來更合理的改法」,兩個人會挑不同行、得到相反結論,而沒有仲裁依據。
- 反推目標行:讀測試名與其斷言,問「它宣稱看守什麼條件」,找出實作中決定該條件的那一行。宣稱與實作對不上時,該測試本身即屬覆蓋錯位族。測試名只描述回傳值而不宣稱條件時,改走反向——先列被測函式的分支,再回頭問哪條測試宣稱看守它
- 套用變異表:母規則是語意反轉,依目標行的性質選類別
- 控制流類:
break改continue、迴圈步進值減一、比較運算子反向、提前退出改為繼續、常數邊界加減一 - 值替換類:回傳值改為 null 或預設值、布林運算子互換、條件整體真值化
- 副作用類:刪除一次副作用呼叫、例外路徑改為吞噬
- 控制流類:
- 在複本上注入:複製被測模組到臨時目錄再改,不對正式檔案注入。執行者中途失敗時,正式檔案會被留在被注入狀態且靜默失效
- 跑測試,確認工作區乾淨:以版本控制狀態驗證,不依賴記得還原
| 結果 | 結論 |
|---|---|
| 有測試變紅 | 該分支確實被看守 |
| 全綠 | 兩種可能,尚不能下結論 |
變異不紅有兩種解釋:變異無害,或有害但現有案例不區分。要分辨必須自行構造區分案例——找出一個輸入,使正確版與變異版產生不同結果。構造不出來才能判定變異無害。停在「變異不紅」就判定無害,會放行未被看守的承重分支。
回傳值等價但可觀測的變異(如快取命中導致的呼叫次數差異)仍屬有害——區分案例可以是呼叫次數而非回傳值。
驗收補測試類任務時:測試數量是輸入,能抓到變異才是輸出。要求對方證明新測試殺得掉變異,而非只回報測試數增加。
免費的變異:修復前的紅綠分佈
修 bug 時不需另外注入變異——缺陷本身就是變異,修復前的程式碼就是變異版。新測試在修復前的紅綠分佈是這次變異測試的結果,且它自然發生、不需複本、不需還原工作區。
判讀方式是比對「紅綠界線」與「缺陷邊界」:
| 修復前分佈 | 結論 |
|---|---|
| 全綠 | 測試沒碰到缺陷。新測試不看守被修的行為,補了等於沒補 |
| 全紅 | 尚不能下結論。可能測到了缺陷,也可能連正確行為都寫錯而全數失敗 |
| 部分紅,界線與缺陷邊界重合 | 測試看守的正是被修的那條線,且對照組證明它不會誤報 |
第三種是唯一能單獨成立的證據——它同時證明測試對缺陷敏感(紅的那些)與對正確行為不誤報(綠的那些)。前兩種都需要補做真正的變異測試才能下結論。
Action:修 bug 時先寫測試再改程式碼,並把修復前的紅綠數字與紅綠界線寫進 ticket 的 Test Results。只寫「修復後全綠」不構成證據——那是所有測試的共同狀態,包含沒測到東西的那些。
邊界:本節只適用於「有明確缺陷可修」的情境。新功能的測試沒有缺陷版可對照,仍須走上方的注入式變異測試。
何時不適用
探索性 spike、smoke test、以及僅為輔助手動驗證而寫的一次性測試,不需套用五族審查。本 skill 針對的是宣稱提供回歸保護的測試。
不涵蓋覆蓋缺口——完全沒有測試碰過的新程式碼不在五族之內。五族處理的是既有測試的覆蓋失真,不是覆蓋的有無。
定位與邊界
正交軸是斷言的區別力對比斷言的穩定性:
| 問題 | Skill |
|---|---|
| 這條斷言即使穩定也驗不到東西 | test-effectiveness(本檔) |
| 這條斷言驗得到東西但結果不穩 | test-assertion-design |
不以紅燈或綠燈分流——環境依附族的紅燈與綠燈是同一枚硬幣的兩面,用該軸切不開。同一測試若兩種問題並存(如測試順序污染:單獨跑紅、全套跑綠且綠燈與程式碼無關),兩檔判準皆適用,不做互斥路由。
其他相鄰職責:TDD 階段推進見 tdd skill;非同步資源清理見 dart-test-async-guardian skill;具體替身框架 API 屬語言與框架文件。
相關 error-pattern
| Pattern | 關係 |
|---|---|
PC-165 |
測試綠燈不等於 Runtime 正確——本 skill 是該主張的可操作化 |
TEST-BAL-002 |
測試替身走簡化建構路徑,屬覆蓋歸零族 |
ARCH-BAL-012 |
覆蓋缺口而非覆蓋失真,列此供邊界對照 |
版本紀錄在同目錄的 CHANGELOG.md。