# Test Effectiveness

> Verifies whether tests actually verify anything. Use when reviewing test coverage or quality, or when tests pass while production breaks. Triggers: 覆蓋率, 測試品質, 測試沒抓到, 過度 mock, 假綠燈, 變異測試, mutation testing. Not for unstable results.

- Skill: `tarrragon/test-effectiveness` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tarrragon/test-effectiveness`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tarrragon/test-effectiveness/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tarrragon (https://skillmd.com/u/tarrragon)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/tarrragon/test-effectiveness

---


# Test Effectiveness

判斷「這套測試是否真的在驗東西」。職責：識別覆蓋不真實的形態、提供證明覆蓋真實的手段。

## 核心主張

**綠燈的資訊量比它看起來少。**

測試通過只證明「在被執行的路徑上，斷言成立」。它不證明被測對象被執行過、不證明斷言被執行過、不證明斷言在驗有意義的行為、也不證明失效時會變紅。

## 劃分軸：綠燈在哪個環節失去資訊量

一次有效的測試要走完四個環節：被測對象被執行、斷言被執行、斷言驗對對象、斷言有區別力。任一環節斷掉，綠燈即不再攜帶程式碼正確的資訊。第五族的斷點不在鏈上，而在「綠燈的來源不是程式碼」。

| 斷點 | 族 |
|------|---|
| 被測對象未被執行 | 覆蓋歸零族 |
| 斷言未被執行 | 斷言未執行族 |
| 斷言驗錯對象 | 覆蓋錯位族 |
| 斷言區別力不足 | 否定結果雙義族 |
| 綠燈來源非程式碼 | 環境依附族 |

此軸是本 skill 的分類依據，也是它可被證偽的方式：**遇到落不進五族的形態，是軸不完整而非該形態不存在**。

## 覆蓋歸零族：被測對象未被執行

**判準**：

> 把被測對象換成恆回傳固定值的空殼，斷言是否仍綠？仍綠即有效覆蓋為零。

此問法不依賴替身存在——測試不使用替身時同樣可操作。替身情境是其特例：移除所有替身後若剩下的斷言只在驗標準函式庫行為，等同被測對象從未參與。

| 形態 | 識別訊號 | 為何危險 |
|------|---------|---------|
| 替身掉唯一有風險的函式 | 被測模組中唯一與環境耦合的函式被替身取代 | 剩餘斷言只在驗標準函式庫，被測邏輯零覆蓋 |
| 守護死碼 | 被測函式在生產路徑無呼叫者 | 測試數量計入覆蓋，實際保護不會執行的程式碼 |
| 空跑綠燈 | 對集合迭代斷言，集合為空時無斷言執行 | 被測機制完全失效（回傳空集合）時測試反而綠 |

## 斷言未執行族：斷言根本沒跑到

前一族的斷言完好但沒碰到被測對象；本族的被測對象可能跑了，但**斷言本身從未執行**。兩者的綠燈都不含資訊，斷點不同。

**判準**：

> 刻意讓斷言失敗一次（改斷言的預期值），測試是否變紅？不變紅即屬本族。

| 形態 | 識別訊號 | 為何危險 |
|------|---------|---------|
| 測試未被收集 | 被 skip／tag 排除／檔名不符 runner 的收集樣式／`xit` 殘留 | 計入「測試數」但從未執行，覆蓋率報告與實際脫節 |
| 非同步未等待 | async 測試未 await，斷言在測試判定結束後才失敗 | runner 記綠，失敗訊息出現在下一個測試或完全丟失 |
| 斷言在未執行的路徑內 | 斷言寫在回呼、事件處理器、或被 try-catch 包住 | 回呼未被觸發、例外被吞噬，斷言靜默跳過 |

本族是實務上最高頻的假綠燈來源，且**前一族的判準對它一律誤判**——把被測對象換成空殼，斷言仍然沒跑，測試仍然綠，看起來像「有效覆蓋為零」以外的正常狀態。故本族的判準必須先跑。

## 覆蓋錯位族：斷言驗錯對象

**判準**：

> 構造一個輸入，使被測邏輯錯誤而此測試仍綠。構造得出即屬本族。

不用「答不答得出回歸行為」判定——那是能力測驗，兩個人會給相反結論，且經驗多的那位永遠判「不屬本族」。

| 形態 | 識別訊號 | 為何危險 |
|------|---------|---------|
| 測 bug report 而非機制 | 修復加了 N 條路徑，測試只覆蓋已知反例走的那一條 | 其餘 N-1 條無護欄，同一缺陷換個觸發向量即復發 |
| 把設計缺陷固化成規格 | 測試斷言「因為某個不該有的限制所以不作用」 | 日後修掉該限制時測試會紅，反過來阻止修復 |
| 名實不符 | 測試名宣稱驗證 X（不呼叫某函式、提前返回），斷言只檢查回傳值 | 比沒有測試更糟——讓維護者相信該機制已有護欄 |

## 否定結果雙義族：斷言區別力不足

**「沒輸出」「沒告警」「條件不同」同時符合「正確」與「壞掉」。**只做否定測試無法區分兩者。

| 只有否定測試時 | 必須補的正向對照 |
|--------------|----------------|
| 空目錄不告警 | 放入探針後必須告警（證明還找得到目標） |
| 兩個識別符比對後不相等 | 端對端建立目標後必須命中（證明分支可達） |
| 誤報已消失 | 真實命中仍必須告警（證明沒有矯枉過正） |

**修誤報時，反向案例應多於正向**：誤報消失容易確認，「真實命中還在不在」才是會被漏掉的一半。若只測前者，連「直接刪掉整個偵測器」都會通過。

## 環境依附族：綠燈來源非程式碼

前四族的斷點在「測試到被測對象」這條鏈上；本族的鏈是通的，但綠燈反映的是本機狀態恰好符合預期，不是程式碼正確。

| 形態 | 識別訊號 | 本機綠燈為何不構成證據 |
|------|---------|---------------------|
| 寫死專案識別符 | 框架資產的測試中出現具體專案名稱或路徑片段 | 綠燈只證明本機的專案名稱恰好符合，未驗證任何程式碼行為 |
| 前提檢查用斷言 | 用斷言檢查測試前提，使「前提不成立」轉為 failure 而非 skip | 綠燈只證明前提成立，被測行為是否正確從未被檢查 |
| 定時炸彈 | 測試依賴的狀態，正是某張進行中的任務要消滅的狀態 | 綠燈依賴一個正被主動消滅的狀態，其保護期有上限 |

**檢查手段**：把環境錨點指向空目錄或臨時目錄重跑。錨點依形態而異——寫死識別符對應專案根與 repo 名，前提檢查對應該前提所依賴的外部服務或檔案，定時炸彈對應被依賴的那份狀態。全通過才算不依附本機狀態。

## 驗證手段：變異測試

改一行程式碼，看測試會不會紅。這是目前唯一能把「測試看守了某個分支」從推論變成觀測的手段。

**目標行由測試宣稱決定，不由直覺決定。** 步驟 1 若停在「選一個看起來更合理的改法」，兩個人會挑不同行、得到相反結論，而沒有仲裁依據。

1. **反推目標行**：讀測試名與其斷言，問「它宣稱看守什麼條件」，找出實作中決定該條件的那一行。宣稱與實作對不上時，該測試本身即屬覆蓋錯位族。測試名只描述回傳值而不宣稱條件時，改走反向——先列被測函式的分支，再回頭問哪條測試宣稱看守它
2. **套用變異表**：母規則是語意反轉，依目標行的性質選類別
   - 控制流類：`break` 改 `continue`、迴圈步進值減一、比較運算子反向、提前退出改為繼續、常數邊界加減一
   - 值替換類：回傳值改為 null 或預設值、布林運算子互換、條件整體真值化
   - 副作用類：刪除一次副作用呼叫、例外路徑改為吞噬
3. **在複本上注入**：複製被測模組到臨時目錄再改，不對正式檔案注入。執行者中途失敗時，正式檔案會被留在被注入狀態且靜默失效
4. **跑測試，確認工作區乾淨**：以版本控制狀態驗證，不依賴記得還原

| 結果 | 結論 |
|------|------|
| 有測試變紅 | 該分支確實被看守 |
| 全綠 | **兩種可能，尚不能下結論** |

**變異不紅有兩種解釋**：變異無害，或有害但現有案例不區分。要分辨必須自行構造區分案例——找出一個輸入，使正確版與變異版產生不同結果。構造不出來才能判定變異無害。停在「變異不紅」就判定無害，會放行未被看守的承重分支。

回傳值等價但可觀測的變異（如快取命中導致的呼叫次數差異）仍屬有害——區分案例可以是呼叫次數而非回傳值。

**驗收補測試類任務時**：測試數量是輸入，能抓到變異才是輸出。要求對方證明新測試殺得掉變異，而非只回報測試數增加。

### 免費的變異：修復前的紅綠分佈

修 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`。

