核心原則

測試本身需要被驗證。 一個從沒看過 RED 的測試 = 未驗證的訊號、不是「會抓回歸的測試」。

驗證一個測試真的有用、需要看到兩個訊號:

  1. RED:測試在「該失敗的版本」上失敗(buggy code → 紅)
  2. GREEN:測試在「該通過的版本」上通過(fixed code → 綠)

只看過 GREEN = 不知道測試有沒有 catch 能力;只看過 RED = 不知道修復有沒有真的解問題。兩個都看到 = 測試 + 修復都被驗證

跳過 RED 把驗收標準降到「測試跑得通」、漏掉「測試自己有沒有 bug」這層。


為什麼測試需要被驗證

測試是程式 about 程式、會有 bug

測試本身是程式碼、跟其他程式碼一樣會有 bug:

測試 bug 類型症狀為什麼跳過 RED 看不到
Selector 寫錯永遠抓不到目標元素、assertion always 過GREEN(因為沒 assert 到任何東西)
Assertion 太寬expect(x).toBeDefined() 對 buggy / fixed 都過GREEN(assertion 通過範圍太大)
Setup / fixture 錯測試根本沒跑、報告假性綠GREEN(測試被 skip 但沒人注意)
Race condition / 時機錯Buggy 時剛好在 race window 過、fixed 時也過GREEN(取決於非常規 case)
測試對象選錯測 happy path、bug 在邊界GREEN(沒覆蓋 bug 所在的範圍)

這五種都會讓「跑測試一次就 GREEN」是個假訊號 — 測試 pass 不代表測試 catch 到該 catch 的東西。

RED 是測試的「使用者驗收」

對使用者代碼、我們會用「驗收訊號」(功能跑得對)證明它有用。測試也需要驗收訊號。

「測試 catch 到 bug」這個能力的驗收訊號 = 「在有 bug 的代碼上失敗」。沒看過這個訊號就相信測試 = 跳過驗收。

對應 #42 2 次門檻:一次 GREEN 是低資訊量訊號、RED → GREEN 是 2 次跑(一次 fail 一次 pass)的高資訊量訊號。


多面向:四種情境的 RED-GREEN 應用

情境 1:修 bug

11. 先寫一個 test 重現 bug 為失敗 — 例:「filter 後 0 筆但 source 還有未載入時、應該顯示 explicit empty 而非 silent」
22. 跑測試 → RED(證明測試抓到 bug、bug 真的存在)
33. 修 code
44. 跑測試 → GREEN(證明修對了 + 測試會抓回歸)

跳過第 2 步 = 不知道測試會不會抓到、不知道 bug 真的有沒有。

情境 2:加 feature

11. 寫 acceptance test 描述新 feature 該有的行為
22. 跑測試 → RED(feature 還沒實作、應該 fail;如果 GREEN 就表示 feature 已經存在或測試太寬)
33. 實作 feature
44. 跑測試 → GREEN

加 feature 時跳過 RED 風險:feature 被誤以為實作但實際是 stub、或測試根本沒驗到 feature。

情境 3:Refactor

11. 確認當前測試 GREEN(baseline)
22. Refactor(不改 behavior)
33. 跑測試 → 仍 GREEN

Refactor 不需要 RED — 因為 behavior 沒變。如果 refactor 後變 RED、表示 refactor 改到了 behavior(變成隱性 bug)、要回頭看。

情境 4:偵錯(不確定 bug 是什麼)

11. 寫一個 test 嘗試重現問題
22. 跑測試 → 看是 RED 還是 GREEN:
3   - RED → 重現成功、現在可以著手修
4   - GREEN → 沒重現到 / 測試寫錯 / bug 在別處 → 重新理解 bug
53. 修
64. 跑測試 → GREEN

「看是 RED 還是 GREEN」這個動作本身是 debug 訊號 — 比單純猜根因有用。


「只看 GREEN 不看 RED」是反模式

反模式 1:修完才補測試(Test-after)

11. 修 bug code
22. 寫測試
33. 跑測試 → GREEN
44. ship

問題:測試從沒跑過 buggy code、不知道它能不能抓到 bug。未來 regression 進來、測試可能仍然 GREEN(測試本身有 bug)。

反模式 2:「快速跑一下測試」沒看訊號

11. 寫測試
22. 跑「應該 pass 吧」、不仔細看輸出
33. 看到 PASS → 安心

問題:可能測試 skip 了、可能測試 zero assertions、可能環境錯了。需要看「具體 catch 到什麼」、不只是「是否 PASS」。

反模式 3:測試 PASS 但 coverage 是 0

11. 寫測試 file
22. CI 跑、看到「all green」
33. 沒看 coverage report

問題:測試文件存在但實際沒 import / 沒執行、CI 報告 GREEN 是因為「沒 fail」不是「有 catch」。


不該套用本原則的情境

「先看 RED 再看 GREEN」原則在大多數情境成立、但有合理例外:

情境為什麼不該套用
Pure refactor沒 behavior 變更、本來就 GREEN、RED 反而表示出問題
純探索 / spike不寫測試、用 console / 手動驗證、不在「測試驗收」範圍
Build / config 改動沒邏輯沒 testable behavior、沒測試可言
顯眼的 syntax 錯誤修復改一個 typo、測試會在 build 階段就 fail、不需要刻意 RED

四類共同特徵:沒有「行為差異」可被測試 catch — 本原則建立在「測試該 catch 的事」上、沒事可 catch 時自然不適用。


跟其他抽象層原則的關係

原則跟本卡的關係
#42 2 次門檻一次 GREEN 是低資訊量訊號、RED → GREEN 是 2 次跑(一次 fail 一次 pass)的真訊號
#56 視覺完成 ≠ 功能完成測試 PASS ≠ 測試 verified;同個「訊號需要驗證」結構
#67 寫作便利度跟意圖對齊反相關跳過 RED 是便利(不用切 branch / 不重 build)、走 RED-GREEN 是對齊
#68 驗收的時間軸本卡是 Checkpoint 2「開發中」+ Checkpoint 3「Ship 前」內部的具體協議

本卡是把「測試這個動作本身」放進驗收體系:寫測試是動作、跑測試的訊號才是驗收。動作完成 ≠ 驗收完成。


對應的實作篇

把測試固化的實作 case 都該套用本卡:


Retrospective 補驗證的協議

如果已經修完才寫測試(test-after)、可以 retrospectively 補 RED-GREEN 驗證:

 1# 1. Stash 現有變動 / 切到修前 commit
 2git stash
 3git checkout <pre-fix-commit>
 4
 5# 2. Cherry-pick 測試 commit(或手動複製 test files)
 6git cherry-pick <test-commit>
 7# 或:cp ../tests/foo.spec.ts tests/  # 複製測試檔過來
 8
 9# 3. Build + 跑測試
10make site && npm test
11# 預期:RED(測試抓到 bug)
12
13# 4. 切回 main / 修後版本
14git checkout main
15git stash pop
16
17# 5. 跑測試
18npm test
19# 預期:GREEN

兩次跑 + 兩個訊號(RED + GREEN)都對、測試才被驗證。Retrospective 補驗證 ≠ 不能補 — 比完全跳過 RED 好、比 test-first 弱。

協議已 codify 為 make verify-red-green PRE_FIX=<commit-sha>(見 Makefile)— 五步驟自動化、不需要每次手動 stash / checkout / build / restore。

Self-case:本卡誕生過程的 dogfooding 失敗

本卡是從一次真實的 dogfooding 失敗抽出來的。修 #55 Filter × Source 層錯位 bug 時、流程是:

  1. 修 code(multi-index 策略)
  2. 寫 4 個 Playwright tests
  3. 跑測試 → 4/4 GREEN
  4. 看起來完工

User 問「修改之前有先寫測試確保符合預測狀態嗎」— 才意識到沒走 RED。Retrospective 補驗證後發現:4 個測試只有 1 個真的 catch 到 bug、其他 3 個對 buggy code 也 PASS(placebo 測試)。

強化後(用 network-level + structural assertion 替換弱 invariant):buggy code 上 1/4 PASS、3/4 FAIL。Fixed code 上 4/4 PASS。RED-GREEN 兩個訊號都看到、測試才真的驗證。

如果不做 retrospective、會帶著 3/4 placebo 測試 ship — 表面 4/4 GREEN、實際只有 1 個真的防回歸。「跑得通」≠「會 catch」這個區別、只有走過 RED 才知道

跳過 RED 是 #72 高 ROI 無外部觸發的工作 在測試協議的展現 — 修法不是「下次記得」(L1 紀律會失敗)、是 make verify-red-green PRE_FIX=<sha>(L3 工具觸發)+ pre-commit hook 提醒(L3 結構觸發)。


判讀徵兆

訊號該做的事
寫完測試第一次跑就 GREEN警訊 — 確認測試是不是真的有 catch 能力(覆蓋 bug case 嗎?)
修了 bug 但沒看過該測試 RED 過補 retrospective 驗證、或下次採 test-first
「我等下會跑一下」但沒實際跑跟「我等下會 refactor」同類謊言、補不回來
CI 永遠 GREEN、沒有人改過測試看 coverage、可能測試沒在跑
加了 feature、測試一寫就 GREENfeature 可能已經存在、或測試太寬
測試環境跟 production 環境差太多RED 在 dev 但 prod 仍 fail = 測試環境沒 catch 真實 case

核心原則:測試不是「跑得通就有用」、是「跑出該有的訊號才有用」。RED 是測試的驗收訊號、跳過 = 接受測試本身可能是壞的。RED → GREEN 兩次跑、才證明「測試真的會 catch + 修復真的解掉 bug」。