<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ai-Generated-Code on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ai-generated-code/</link><description>Recent content in Ai-Generated-Code on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 24 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ai-generated-code/index.xml" rel="self" type="application/rss+xml"/><item><title>判準的推導來源：測試憑什麼不是實作的回音</title><link>https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/</guid><description>&lt;p>測試&lt;strong>抓出需求被誤讀&lt;/strong>的能力，取決於它的判準是不是由實作推導出來的。判準另有來源時，測試才有機會撞見實作對需求的誤解；判準從實作推導時，&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle&lt;/a> 在這一項上退化成實作自身的回音。其他幾種驗證力不受影響——崩潰、回歸、內部不一致照樣抓得到，下一節的表把界線畫出來。&lt;/p>
&lt;p>&lt;strong>出處是這條軸最常見的代理，不是這條軸本身。&lt;/strong> 同一次生成的測試與實作幾乎一定共用推導，所以出處同源通常意味著判準被污染；但兩者不是同一件事，而它們分岔的位置恰好是本模組推薦的做法之一——同一個 agent 同時產出實作與一條&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">往返性質&lt;/a>時，出處同源而判準來自需求，驗證力仍在。反過來也成立：兩個獨立會話，而寫測試那一側被餵了既有的 API schema，出處獨立而判準有一部分從實作推導。判定要問的是&lt;strong>這條斷言的預期值從哪裡長出來&lt;/strong>，出處只是最好查的那個線索。這條性質在測試由人手寫的年代很少被單獨討論，因為寫測試的人與寫實作的人即使是同一位，中間也隔著一段時間與一次重新理解需求的動作；產出速度提高之後那段間隔消失了，獨立性變成需要主動維持的性質。&lt;/p>
&lt;h2 id="判準被污染的三種形態">判準被污染的三種形態&lt;/h2>
&lt;p>判準被實作污染的形態由兩件事決定：&lt;strong>寫實作與寫斷言的推理是不是同一段&lt;/strong>，以及&lt;strong>寫斷言時手上有沒有實作可以照抄&lt;/strong>。兩者組合成四格，而第四格「不同推理、無可抄對象」是後面要談的做法（另一個來源先寫測試）、不是問題形態，所以不在這一節。以下三種按判準被污染的程度由重到輕排列。&lt;/p>
&lt;p>&lt;strong>同一次產出（同推理、無可抄對象）。&lt;/strong> 一個請求同時要求實作與測試，兩者由同一段推理產生。獨立性最低，因為需求的每一次誤讀都會被一致地寫進兩邊——這裡沒有可抄的實作，但也不需要抄，誤解在源頭就共用了。&lt;/p>
&lt;p>&lt;strong>實作完成後補測試（不同推理、有可抄對象）。&lt;/strong> 實作先存在，之後要求為它補一套測試。測試會照著實作的分支結構長出來，覆蓋率數字通常很高，而斷言的預期值多半是從實作推導的。推理雖然分了兩段，可抄的對象讓第二段推理沒有動力回到需求。&lt;/p>
&lt;p>&lt;strong>人讀完實作後補測試（不同推理、有可抄對象、但推理者換了）。&lt;/strong> 人讀過產出再寫測試，獨立性取決於讀的過程有沒有回到需求。讀完一份不熟悉的實作之後，腦中的「它該做什麼」已經被它實際做了什麼佔滿，寫出來的斷言常常是實作行為的複述。這一種比多數人以為的低，而它仍是三者中最高的——換了推理者本身就製造了一部分差異。&lt;/p>
&lt;p>三種形態共通的訊號是斷言的寫法：斷言重述實作的計算式（實作寫 &lt;code>total * 0.8&lt;/code>、斷言也算 &lt;code>total * 0.8&lt;/code>）。預期值只要能從實作推導出來，這條斷言就沒有在驗證任何東西。&lt;/p>
&lt;h2 id="判準被污染時還抓得到什麼">判準被污染時還抓得到什麼&lt;/h2>
&lt;p>同源不等於這組測試沒有價值，它的射程被限縮到特定幾類錯誤：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>錯誤類別&lt;/th>
 &lt;th>同源的測試抓不抓得到&lt;/th>
 &lt;th>原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>執行期崩潰、空指標、型別不符&lt;/td>
 &lt;td>抓得到&lt;/td>
 &lt;td>測試跑過那條路徑就會炸，與判準來源無關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>後續改動造成的回歸&lt;/td>
 &lt;td>抓得到&lt;/td>
 &lt;td>基準是這次的行為，而基準只要穩定就能偵測變化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實作內部不一致（兩處算法不同）&lt;/td>
 &lt;td>部分抓得到&lt;/td>
 &lt;td>兩處都被覆蓋且結果被比較時才會現形&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需求被誤讀（順序、方向、邊界的語意）&lt;/td>
 &lt;td>抓不到&lt;/td>
 &lt;td>誤讀同時存在於實作與斷言，兩邊一致所以全綠&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需求缺漏（沒有人想到的情境）&lt;/td>
 &lt;td>抓不到&lt;/td>
 &lt;td>沒有進入實作的情境也不會進入測試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最後兩類正是 agent 產出最容易出錯的地方，也是同源測試的系統性盲區。單據合併後明細 id 全部重建（下游持有舊 id 的取消與追加操作從此無聲失效，而畫面看起來一切正常）、危險判定的先後順序、幣別換算的方向、狀態機少一條轉換——這些都是需求語意層的問題，在實作與測試共用同一個誤解時全部通過。&lt;/p>
&lt;p>這個盲區不會因為測試變多而縮小。&lt;a href="https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10&lt;/a> 裡八份程式的單元測試規模從零到 206 個範例，而 25 條驗收條件對危險判定順序一律沉默——規模成長沒有把任何一份帶進那個維度，因為斷言全部從同一份對需求的理解長出來。&lt;/p>
&lt;p>本節的三種形態與四個變數出自機制推導；用來對照的實驗數字只有一份來源，&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/" data-link-title="模組六：Agent 產出程式碼的驗證" data-link-desc="程式碼由 agent 寫、人不再逐行讀時，用來決定驗證該落在哪些位置，以及每個位置的射程到哪裡">模組入口&lt;/a>交代了它支持與不支持哪些宣稱。&lt;/p>
&lt;h2 id="把判準移出實作的四個變數">把判準移出實作的四個變數&lt;/h2>
&lt;p>獨立性可以被設計，能動的有四件事。前三件是本節展開的重點，第四件在開場已經出現過——&lt;strong>時間間隔&lt;/strong>：寫實作與寫測試之間隔一段時間，中間發生一次重新理解需求的動作。它在人手寫的年代是免費附贈的，產出速度提高之後要刻意安排，而它也是四者裡最不可靠的一個（間隔不保證有人真的回去讀需求）。&lt;/p>
&lt;p>&lt;strong>需求的來源。&lt;/strong> 把驗收條件寫成一份人審過的文件，測試的預期值從那份文件逐條取得。這是四者裡效果最強的一個，因為它直接把 oracle 的來源移到實作之外。這份文件自己會漂移，而漂移的方向剛好抵銷它的作用——實作改了之後有人回來把它「對齊現況」，判準就在維護環節被實作污染了。所以它要跟程式碼同 repo、同一個 PR 送審，而且變更要由不是實作者的人核可；做不到這一點時，依&lt;a href="https://tarrragon.github.io/blog/report/doc-sync-needs-mechanism-or-demotion/" data-link-title="多份文件必然漂移：同步期待要嘛有機制承接、要嘛明示降級" data-link-desc="設計開發流程的文件鏈（proposal、spec、UC、設計文件、追溯表、測試、註解）、或審查一套流程的文件模型時使用。判準是每份文件的同步期待與守護機制是否匹配：期待最新就要有機制守著、沒有機制就降級為一次性 scaffold 或 append-only 記錄。">同步期待要嘛有機制承接、要嘛明示降級&lt;/a>把它降級成標記消費時點的記錄，不要繼續當它是最新的。成本落在那份文件必須寫到可以逐條對應成斷言的程度——寫成「訂單要正確處理」沒有用，要寫到「同一個房間裡同時有蝙蝠與陷阱時，先觸發哪一個」這種顆粒度。條件顆粒度的設計在&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類&lt;/a>那章展開。&lt;/p>
&lt;p>&lt;strong>產出的順序。&lt;/strong> 測試先寫、實作後補，讓實作沒有可照抄的對象。這正是 Test-First 在 agent 產出情境下的新理由——它原本的理由是「小步好想、介面問題早暴露」，服務的是人的認知負荷；現在它多了一個結構性的理由，就是製造出處差異。順序帶來的獨立性有上限：先寫的測試如果也由同一個 agent 從同一段需求描述生成，誤讀仍然會一致地進入兩邊，順序只擋住了「照著實作寫斷言」這一種退化。&lt;/p>
&lt;p>&lt;strong>產出的主體。&lt;/strong> 測試與實作由不同的 agent、不同的會話、或不同的人產出，且寫測試的一方看不到實作。判準是&lt;strong>寫測試那一側看不到實作&lt;/strong>；「開兩個獨立的會話、只餵需求不餵程式碼」是寫作當下達成它的方式，而不是判準本身——跨會話記憶或共用專案 context 會讓兩個會話不再等於隔離，用之前先確認手上的工具是哪一種，成本是需求描述必須自足——寫測試那一側沒有實作可以參照，需求裡漏掉的部分就會直接變成測試的缺口。這個成本本身是有用的訊號，但它有一個判不開的地方：寫不出測試也可能是那一方缺領域上下文或經驗不足，同一個觀察對應得到兩個原因。要分開只有一種做法——同一份需求給兩個獨立的測試方，兩方卡在同一處才是需求的問題。&lt;/p>
&lt;p>&lt;strong>主體這條線往上還有兩層，而本模組其餘部分預設它們不存在。&lt;/strong> 「產出的主體」講的是 agent 與會話，那是最裡面那一層；外面還有組織層與契約層：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>這一層的主體是誰&lt;/th>
 &lt;th>它決定什麼&lt;/th>
 &lt;th>不在同一方時會怎樣&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>產出&lt;/td>
 &lt;td>哪個 agent、哪次會話&lt;/td>
 &lt;td>判準與實作共不共用推導&lt;/td>
 &lt;td>本節四個變數處理的就是這一層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組織&lt;/td>
 &lt;td>寫需求的人、寫實作的人、驗收的人&lt;/td>
 &lt;td>誰有能力寫出可對應成斷言的條件&lt;/td>
 &lt;td>外包時驗收測試由乙方寫是同源的組織版；改由甲方寫則甲方多半沒有那個能力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>契約與監理&lt;/td>
 &lt;td>客戶、主管機關、內稽&lt;/td>
 &lt;td>哪些閘門是外部義務、不由團隊決定&lt;/td>
 &lt;td>覆蓋率門檻寫進合約、或 code review 是變更管理控制項時，本模組的修法會與義務衝突&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三層的判準相同（判準是不是由實作推導），可動的手段不同。本模組後面三章的修法都寫在產出層——&lt;strong>決定權、滿足義務、後果承擔屬於同一方&lt;/strong>是它們共同的前提。這個前提不成立時，先確認是哪一層的主體不同，再看那一層有沒有可動的變數；契約層通常沒有可動的變數，此時要動的是合約或控制程序本身，那超出本模組的射程——但&lt;strong>不代表沒有事情可做&lt;/strong>：外部門檻照舊守著、內部指標另外疊加上去，那條原則在&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替&lt;/a>的導入順序段。&lt;/p>
&lt;p>四者可以疊加，也可以只動一件——但只動一件時要知道拿回了多少：動「需求的來源」把判準整個移出實作，語意層的盲區關得起來；只動順序或時間間隔只擋掉「照著實作寫斷言」這一種退化，共用誤讀那一種仍在，此時定位仍然是回歸網。都動不了的情況下，這組測試的定位要誠實降級成回歸網——它守得住「後來改壞了」，守不住「一開始就寫錯了」——並且在別處補上獨立的判準。&lt;/p>
&lt;h2 id="人審的位置要跟著移動">人審的位置要跟著移動&lt;/h2>
&lt;p>逐行讀 agent 寫的實作，讀的人已經被實作的敘事引導：變數命名、註解、結構都在說服讀者這段程式做的就是它該做的事，而讀者手上沒有一份獨立的對照。這種閱讀能發現的多半是風格與局部缺陷，發現不了「整段做的是另一件事」。&lt;/p>
&lt;p>同樣的注意力放在驗收條件上，審的是需求本身，而在需求、驗收條件、實作、測試這條鏈上，驗收條件是少數還沒有被實作污染的環節之一。這也是判斷「不再逐行讀產出」這個決定是否成立的方式：說得出哪一個環節接手了原本由閱讀承擔的那件事，這個決定就成立；說不出來時，交換掉的是驗證的可見性而不是閱讀的時間。&lt;/p>
&lt;h2 id="邊界判準取自實作也合理的場合">邊界：判準取自實作也合理的場合&lt;/h2>
&lt;p>一次性的探索腳本、用完就丟的資料轉換（「用完就丟」是對未來的預測，要說得出丟棄的時點或條件，說不出來的多半不會被丟）、以及&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">characterization test&lt;/a> 這種本來就以現狀為判準的測試，同源不構成問題——前兩者的錯誤代價低到不值得建立獨立判準，後者的設計意圖就是複製現狀。&lt;/p>
&lt;p>重構是另一種邊界，而且它的正確配置不在本節四個變數裡：重構的 oracle 是&lt;strong>改動前的行為&lt;/strong>而不是需求文件，所以「把驗收條件寫成人審過的文件」在這裡等於重建二十年的規格。可行的配置是拿舊實作當&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">參照 oracle&lt;/a>（新舊並行跑、比對輸出），配上&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">行為快照&lt;/a>守住尚未被正確性測試覆蓋的部分。&lt;/p>
&lt;p>另一個邊界是需求本身沒有語意深度的功能：CRUD 端點的欄位對映、單純的格式轉換，誤讀的空間小，同源測試的盲區也隨之縮小。判斷方式是問這個功能有沒有「兩件事同時發生時該怎麼辦」這類問題，沒有的話同源的風險確實低。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>斷言的預期值是實作算式的複述&lt;/td>
 &lt;td>回到需求文件取預期值；取不到就是需求沒寫到那個顆粒度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次請求同時產出實作與測試&lt;/td>
 &lt;td>拆成兩次，並讓寫測試的那次看不到實作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>覆蓋率數字很高而線上故障集中在語意層（順序、方向）&lt;/td>
 &lt;td>這是同源的典型指紋，補測試沒有用，要補的是獨立的驗收條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「我讀過了，測試也是照著需求寫的」&lt;/td>
 &lt;td>問寫測試的當下手上有沒有實作；有的話獨立性比想像中低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需求描述短到另一個人無法據以寫出測試&lt;/td>
 &lt;td>那份描述也不足以讓實作正確，先補需求再談測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>決定不再逐行讀產出&lt;/td>
 &lt;td>說出哪一個環節接手了原本由閱讀承擔的那件事，說不出來就先不要做這個決定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>驗收條件要寫到什麼顆粒度才擋得住語意層錯誤 → &lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類&lt;/a>&lt;/li>
&lt;li>有了獨立判準之後怎麼量測試的偵測能力 → &lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替&lt;/a>&lt;/li>
&lt;li>這條性質的術語卡 → &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">Test Provenance&lt;/a>&lt;/li>
&lt;li>由測試自己餵資料造成的同型盲區 → &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/stub/" data-link-title="Stub" data-link-desc="測試作者手動寫死回應資料的 test double：驗證的是假設成立時邏輯是否正確，假設本身錯誤時無法檢出">Stub&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5&lt;/a>&lt;/li>
&lt;li>站內在測試耦合對象上的立場 → &lt;a href="https://tarrragon.github.io/blog/record/behavior-first-tdd-methodology/" data-link-title="行為優先的 TDD：測試耦合行為、不耦合結構" data-link-desc="重構時測試大量壞掉、或在「該不該 mock 這個協作者」上需要一個可執行的立場時，本站對兩派 TDD 分歧的選擇與推導">行為優先的 TDD&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>測試<strong>抓出需求被誤讀</strong>的能力，取決於它的判準是不是由實作推導出來的。判準另有來源時，測試才有機會撞見實作對需求的誤解；判準從實作推導時，<a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle</a> 在這一項上退化成實作自身的回音。其他幾種驗證力不受影響——崩潰、回歸、內部不一致照樣抓得到，下一節的表把界線畫出來。</p>
<p><strong>出處是這條軸最常見的代理，不是這條軸本身。</strong> 同一次生成的測試與實作幾乎一定共用推導，所以出處同源通常意味著判準被污染；但兩者不是同一件事，而它們分岔的位置恰好是本模組推薦的做法之一——同一個 agent 同時產出實作與一條<a href="/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">往返性質</a>時，出處同源而判準來自需求，驗證力仍在。反過來也成立：兩個獨立會話，而寫測試那一側被餵了既有的 API schema，出處獨立而判準有一部分從實作推導。判定要問的是<strong>這條斷言的預期值從哪裡長出來</strong>，出處只是最好查的那個線索。這條性質在測試由人手寫的年代很少被單獨討論，因為寫測試的人與寫實作的人即使是同一位，中間也隔著一段時間與一次重新理解需求的動作；產出速度提高之後那段間隔消失了，獨立性變成需要主動維持的性質。</p>
<h2 id="判準被污染的三種形態">判準被污染的三種形態</h2>
<p>判準被實作污染的形態由兩件事決定：<strong>寫實作與寫斷言的推理是不是同一段</strong>，以及<strong>寫斷言時手上有沒有實作可以照抄</strong>。兩者組合成四格，而第四格「不同推理、無可抄對象」是後面要談的做法（另一個來源先寫測試）、不是問題形態，所以不在這一節。以下三種按判準被污染的程度由重到輕排列。</p>
<p><strong>同一次產出（同推理、無可抄對象）。</strong> 一個請求同時要求實作與測試，兩者由同一段推理產生。獨立性最低，因為需求的每一次誤讀都會被一致地寫進兩邊——這裡沒有可抄的實作，但也不需要抄，誤解在源頭就共用了。</p>
<p><strong>實作完成後補測試（不同推理、有可抄對象）。</strong> 實作先存在，之後要求為它補一套測試。測試會照著實作的分支結構長出來，覆蓋率數字通常很高，而斷言的預期值多半是從實作推導的。推理雖然分了兩段，可抄的對象讓第二段推理沒有動力回到需求。</p>
<p><strong>人讀完實作後補測試（不同推理、有可抄對象、但推理者換了）。</strong> 人讀過產出再寫測試，獨立性取決於讀的過程有沒有回到需求。讀完一份不熟悉的實作之後，腦中的「它該做什麼」已經被它實際做了什麼佔滿，寫出來的斷言常常是實作行為的複述。這一種比多數人以為的低，而它仍是三者中最高的——換了推理者本身就製造了一部分差異。</p>
<p>三種形態共通的訊號是斷言的寫法：斷言重述實作的計算式（實作寫 <code>total * 0.8</code>、斷言也算 <code>total * 0.8</code>）。預期值只要能從實作推導出來，這條斷言就沒有在驗證任何東西。</p>
<h2 id="判準被污染時還抓得到什麼">判準被污染時還抓得到什麼</h2>
<p>同源不等於這組測試沒有價值，它的射程被限縮到特定幾類錯誤：</p>
<table>
  <thead>
      <tr>
          <th>錯誤類別</th>
          <th>同源的測試抓不抓得到</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>執行期崩潰、空指標、型別不符</td>
          <td>抓得到</td>
          <td>測試跑過那條路徑就會炸，與判準來源無關</td>
      </tr>
      <tr>
          <td>後續改動造成的回歸</td>
          <td>抓得到</td>
          <td>基準是這次的行為，而基準只要穩定就能偵測變化</td>
      </tr>
      <tr>
          <td>實作內部不一致（兩處算法不同）</td>
          <td>部分抓得到</td>
          <td>兩處都被覆蓋且結果被比較時才會現形</td>
      </tr>
      <tr>
          <td>需求被誤讀（順序、方向、邊界的語意）</td>
          <td>抓不到</td>
          <td>誤讀同時存在於實作與斷言，兩邊一致所以全綠</td>
      </tr>
      <tr>
          <td>需求缺漏（沒有人想到的情境）</td>
          <td>抓不到</td>
          <td>沒有進入實作的情境也不會進入測試</td>
      </tr>
  </tbody>
</table>
<p>最後兩類正是 agent 產出最容易出錯的地方，也是同源測試的系統性盲區。單據合併後明細 id 全部重建（下游持有舊 id 的取消與追加操作從此無聲失效，而畫面看起來一切正常）、危險判定的先後順序、幣別換算的方向、狀態機少一條轉換——這些都是需求語意層的問題，在實作與測試共用同一個誤解時全部通過。</p>
<p>這個盲區不會因為測試變多而縮小。<a href="/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10</a> 裡八份程式的單元測試規模從零到 206 個範例，而 25 條驗收條件對危險判定順序一律沉默——規模成長沒有把任何一份帶進那個維度，因為斷言全部從同一份對需求的理解長出來。</p>
<p>本節的三種形態與四個變數出自機制推導；用來對照的實驗數字只有一份來源，<a href="/blog/testing/06-agent-authored-code/" data-link-title="模組六：Agent 產出程式碼的驗證" data-link-desc="程式碼由 agent 寫、人不再逐行讀時，用來決定驗證該落在哪些位置，以及每個位置的射程到哪裡">模組入口</a>交代了它支持與不支持哪些宣稱。</p>
<h2 id="把判準移出實作的四個變數">把判準移出實作的四個變數</h2>
<p>獨立性可以被設計，能動的有四件事。前三件是本節展開的重點，第四件在開場已經出現過——<strong>時間間隔</strong>：寫實作與寫測試之間隔一段時間，中間發生一次重新理解需求的動作。它在人手寫的年代是免費附贈的，產出速度提高之後要刻意安排，而它也是四者裡最不可靠的一個（間隔不保證有人真的回去讀需求）。</p>
<p><strong>需求的來源。</strong> 把驗收條件寫成一份人審過的文件，測試的預期值從那份文件逐條取得。這是四者裡效果最強的一個，因為它直接把 oracle 的來源移到實作之外。這份文件自己會漂移，而漂移的方向剛好抵銷它的作用——實作改了之後有人回來把它「對齊現況」，判準就在維護環節被實作污染了。所以它要跟程式碼同 repo、同一個 PR 送審，而且變更要由不是實作者的人核可；做不到這一點時，依<a href="/blog/report/doc-sync-needs-mechanism-or-demotion/" data-link-title="多份文件必然漂移：同步期待要嘛有機制承接、要嘛明示降級" data-link-desc="設計開發流程的文件鏈（proposal、spec、UC、設計文件、追溯表、測試、註解）、或審查一套流程的文件模型時使用。判準是每份文件的同步期待與守護機制是否匹配：期待最新就要有機制守著、沒有機制就降級為一次性 scaffold 或 append-only 記錄。">同步期待要嘛有機制承接、要嘛明示降級</a>把它降級成標記消費時點的記錄，不要繼續當它是最新的。成本落在那份文件必須寫到可以逐條對應成斷言的程度——寫成「訂單要正確處理」沒有用，要寫到「同一個房間裡同時有蝙蝠與陷阱時，先觸發哪一個」這種顆粒度。條件顆粒度的設計在<a href="/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類</a>那章展開。</p>
<p><strong>產出的順序。</strong> 測試先寫、實作後補，讓實作沒有可照抄的對象。這正是 Test-First 在 agent 產出情境下的新理由——它原本的理由是「小步好想、介面問題早暴露」，服務的是人的認知負荷；現在它多了一個結構性的理由，就是製造出處差異。順序帶來的獨立性有上限：先寫的測試如果也由同一個 agent 從同一段需求描述生成，誤讀仍然會一致地進入兩邊，順序只擋住了「照著實作寫斷言」這一種退化。</p>
<p><strong>產出的主體。</strong> 測試與實作由不同的 agent、不同的會話、或不同的人產出，且寫測試的一方看不到實作。判準是<strong>寫測試那一側看不到實作</strong>；「開兩個獨立的會話、只餵需求不餵程式碼」是寫作當下達成它的方式，而不是判準本身——跨會話記憶或共用專案 context 會讓兩個會話不再等於隔離，用之前先確認手上的工具是哪一種，成本是需求描述必須自足——寫測試那一側沒有實作可以參照，需求裡漏掉的部分就會直接變成測試的缺口。這個成本本身是有用的訊號，但它有一個判不開的地方：寫不出測試也可能是那一方缺領域上下文或經驗不足，同一個觀察對應得到兩個原因。要分開只有一種做法——同一份需求給兩個獨立的測試方，兩方卡在同一處才是需求的問題。</p>
<p><strong>主體這條線往上還有兩層，而本模組其餘部分預設它們不存在。</strong> 「產出的主體」講的是 agent 與會話，那是最裡面那一層；外面還有組織層與契約層：</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>這一層的主體是誰</th>
          <th>它決定什麼</th>
          <th>不在同一方時會怎樣</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>產出</td>
          <td>哪個 agent、哪次會話</td>
          <td>判準與實作共不共用推導</td>
          <td>本節四個變數處理的就是這一層</td>
      </tr>
      <tr>
          <td>組織</td>
          <td>寫需求的人、寫實作的人、驗收的人</td>
          <td>誰有能力寫出可對應成斷言的條件</td>
          <td>外包時驗收測試由乙方寫是同源的組織版；改由甲方寫則甲方多半沒有那個能力</td>
      </tr>
      <tr>
          <td>契約與監理</td>
          <td>客戶、主管機關、內稽</td>
          <td>哪些閘門是外部義務、不由團隊決定</td>
          <td>覆蓋率門檻寫進合約、或 code review 是變更管理控制項時，本模組的修法會與義務衝突</td>
      </tr>
  </tbody>
</table>
<p>三層的判準相同（判準是不是由實作推導），可動的手段不同。本模組後面三章的修法都寫在產出層——<strong>決定權、滿足義務、後果承擔屬於同一方</strong>是它們共同的前提。這個前提不成立時，先確認是哪一層的主體不同，再看那一層有沒有可動的變數；契約層通常沒有可動的變數，此時要動的是合約或控制程序本身，那超出本模組的射程——但<strong>不代表沒有事情可做</strong>：外部門檻照舊守著、內部指標另外疊加上去，那條原則在<a href="/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替</a>的導入順序段。</p>
<p>四者可以疊加，也可以只動一件——但只動一件時要知道拿回了多少：動「需求的來源」把判準整個移出實作，語意層的盲區關得起來；只動順序或時間間隔只擋掉「照著實作寫斷言」這一種退化，共用誤讀那一種仍在，此時定位仍然是回歸網。都動不了的情況下，這組測試的定位要誠實降級成回歸網——它守得住「後來改壞了」，守不住「一開始就寫錯了」——並且在別處補上獨立的判準。</p>
<h2 id="人審的位置要跟著移動">人審的位置要跟著移動</h2>
<p>逐行讀 agent 寫的實作，讀的人已經被實作的敘事引導：變數命名、註解、結構都在說服讀者這段程式做的就是它該做的事，而讀者手上沒有一份獨立的對照。這種閱讀能發現的多半是風格與局部缺陷，發現不了「整段做的是另一件事」。</p>
<p>同樣的注意力放在驗收條件上，審的是需求本身，而在需求、驗收條件、實作、測試這條鏈上，驗收條件是少數還沒有被實作污染的環節之一。這也是判斷「不再逐行讀產出」這個決定是否成立的方式：說得出哪一個環節接手了原本由閱讀承擔的那件事，這個決定就成立；說不出來時，交換掉的是驗證的可見性而不是閱讀的時間。</p>
<h2 id="邊界判準取自實作也合理的場合">邊界：判準取自實作也合理的場合</h2>
<p>一次性的探索腳本、用完就丟的資料轉換（「用完就丟」是對未來的預測，要說得出丟棄的時點或條件，說不出來的多半不會被丟）、以及<a href="/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">characterization test</a> 這種本來就以現狀為判準的測試，同源不構成問題——前兩者的錯誤代價低到不值得建立獨立判準，後者的設計意圖就是複製現狀。</p>
<p>重構是另一種邊界，而且它的正確配置不在本節四個變數裡：重構的 oracle 是<strong>改動前的行為</strong>而不是需求文件，所以「把驗收條件寫成人審過的文件」在這裡等於重建二十年的規格。可行的配置是拿舊實作當<a href="/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">參照 oracle</a>（新舊並行跑、比對輸出），配上<a href="/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">行為快照</a>守住尚未被正確性測試覆蓋的部分。</p>
<p>另一個邊界是需求本身沒有語意深度的功能：CRUD 端點的欄位對映、單純的格式轉換，誤讀的空間小，同源測試的盲區也隨之縮小。判斷方式是問這個功能有沒有「兩件事同時發生時該怎麼辦」這類問題，沒有的話同源的風險確實低。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>斷言的預期值是實作算式的複述</td>
          <td>回到需求文件取預期值；取不到就是需求沒寫到那個顆粒度</td>
      </tr>
      <tr>
          <td>一次請求同時產出實作與測試</td>
          <td>拆成兩次，並讓寫測試的那次看不到實作</td>
      </tr>
      <tr>
          <td>覆蓋率數字很高而線上故障集中在語意層（順序、方向）</td>
          <td>這是同源的典型指紋，補測試沒有用，要補的是獨立的驗收條件</td>
      </tr>
      <tr>
          <td>「我讀過了，測試也是照著需求寫的」</td>
          <td>問寫測試的當下手上有沒有實作；有的話獨立性比想像中低</td>
      </tr>
      <tr>
          <td>需求描述短到另一個人無法據以寫出測試</td>
          <td>那份描述也不足以讓實作正確，先補需求再談測試</td>
      </tr>
      <tr>
          <td>決定不再逐行讀產出</td>
          <td>說出哪一個環節接手了原本由閱讀承擔的那件事，說不出來就先不要做這個決定</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>驗收條件要寫到什麼顆粒度才擋得住語意層錯誤 → <a href="/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類</a></li>
<li>有了獨立判準之後怎麼量測試的偵測能力 → <a href="/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替</a></li>
<li>這條性質的術語卡 → <a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">Test Provenance</a></li>
<li>由測試自己餵資料造成的同型盲區 → <a href="/blog/testing/knowledge-cards/stub/" data-link-title="Stub" data-link-desc="測試作者手動寫死回應資料的 test double：驗證的是假設成立時邏輯是否正確，假設本身錯誤時無法檢出">Stub</a> 與 <a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5</a></li>
<li>站內在測試耦合對象上的立場 → <a href="/blog/record/behavior-first-tdd-methodology/" data-link-title="行為優先的 TDD：測試耦合行為、不耦合結構" data-link-desc="重構時測試大量壞掉、或在「該不該 mock 這個協作者」上需要一個可執行的立場時，本站對兩派 TDD 分歧的選擇與推導">行為優先的 TDD</a></li>
</ul>
]]></content:encoded></item><item><title>驗收條件的等價類：關卡實際固定住了什麼</title><link>https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/</guid><description>&lt;p>一組驗收條件通過，證明的是產出落在該條件切出的等價類裡（這裡的等價類指&lt;strong>能通過同一組條件的所有程式&lt;/strong>所成的集合，跟測試設計裡的等價類劃分不是同一件事——那個切的是輸入域，這個切的是程式）。等價類是所有能通過這組條件的程式所成的集合，它的大小由條件之間的&lt;strong>交叉密度&lt;/strong>決定，不由條件的數量決定。設計驗收條件的工作因此不是「把功能講完」，而是把等價類縮到只剩下真正屬於實作自由度的差異。&lt;/p>
&lt;p>閱讀退出之後，「那兩個同時發生怎麼辦」這句話沒有人會順口問了——它原本由讀程式碼的人隨手補上，現在要由條件的設計自己回答。&lt;/p>
&lt;h2 id="等價類可以被量出來">等價類可以被量出來&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10&lt;/a> 把這件事實際量了一次。產品是 Hunt the Wumpus，一個主控台文字冒險遊戲：玩家在洞穴間移動、房間裡可能有怪物、陷阱或蝙蝠。同一份需求從空目錄寫八次，八個程式全部通過同一組 25 案例的驗收，八棵原始碼樹沒有任何兩棵相同，行數從 331 到 603。&lt;/p>
&lt;p>被放行的差異裡，大部分屬於實作自由度——檔案怎麼切、函式怎麼命名、內部用什麼資料結構，這些本來就該留給實作。但有三類不是：&lt;/p>
&lt;p>&lt;strong>判定順序。&lt;/strong> 原規則是先檢查怪物、再陷阱、再蝙蝠。八個版本裡四個照做、三個反過來先檢查蝙蝠、一個的紀錄沒寫順序——兩種順序都通過，因為沒有任何一條驗收案例把兩個危險放進同一個房間。&lt;/p>
&lt;p>&lt;strong>終止方式。&lt;/strong> 一個版本的主迴圈永不結束，一個外層迴圈永不離開，一個以拋出並吞掉例外的方式收場。驗收沒有斷言離開的方式。&lt;/p>
&lt;p>&lt;strong>亂數來源。&lt;/strong> 三種不同的產生器，意味著同一個 seed 參數在不同版本產出不同結果——&lt;code>--seed&lt;/code> 沒有成為可攜的契約。&lt;/p>
&lt;p>這三類都是需求的一部分，只是沒有被寫進條件裡。&lt;strong>它們與實作自由度的差別在於：改變它們會改變使用者觀察得到的行為。&lt;/strong>&lt;/p>
&lt;h2 id="密度與涵蓋面是兩件事">密度與涵蓋面是兩件事&lt;/h2>
&lt;p>&lt;strong>同一維度上多加一條斷言，縮小等價類的幅度隨密度遞減；而對一個沒有任何條件碰過的維度，增量是零。&lt;/strong> 這條判準的操作含義是補到第十個邊界值的收益遠低於補第一條「兩件事同時發生」。&lt;/p>
&lt;p>同一個實驗提供了它的外顯形態：八份程式的單元測試規模從零到 206 個範例，驗收結果卻完全一致——那 25 條驗收條件把每個危險單獨出現時的反應驗了很多遍，兩個危險同時出現一次都沒有，所以判定順序這個維度的增量始終是零。&lt;/p>
&lt;p>它同時解釋了一種常見的挫折感——測試一直加、線上還是出事，而出的事「測試裡明明有類似的案例」。類似不等於交叉：同一個維度上的第十個案例，對另一個維度的貢獻仍然是零。&lt;/p>
&lt;p>本章引用的量測數字全部來自同一份實驗，它撐得起與撐不起什麼寫在&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/" data-link-title="模組六：Agent 產出程式碼的驗證" data-link-desc="程式碼由 agent 寫、人不再逐行讀時，用來決定驗證該落在哪些位置，以及每個位置的射程到哪裡">模組入口&lt;/a>。&lt;/p>
&lt;h2 id="量射程的操作方式">量射程的操作方式&lt;/h2>
&lt;p>把條件攤成維度表，看哪些維度組合沒有被任何一條條件同時碰到。維度自己要先推出來，推法是逐一問「這個系統的行為會隨著什麼而不同」——答案裡每一個獨立的自變數就是一個維度。&lt;/p>
&lt;p>下面用一個具名流程走一次，讓推導的結果看得見：&lt;strong>帳號權限異動&lt;/strong>（管理者調整某位使用者的角色，而該使用者可能正在使用系統）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>這個流程裡它是什麼&lt;/th>
 &lt;th>交叉的形態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>狀態&lt;/td>
 &lt;td>使用者當下是登入中、閒置、還是已停用&lt;/td>
 &lt;td>對一個登入中的使用者降權，他手上那張 session 怎麼算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件&lt;/td>
 &lt;td>收到的是升權、降權、還是整組角色置換&lt;/td>
 &lt;td>升權與降權在同一個時間窗內各來一次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>順序&lt;/td>
 &lt;td>兩次異動的先後&lt;/td>
 &lt;td>先降後升與先升後降的最終權限該不該相同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>併發&lt;/td>
 &lt;td>兩位管理者同時改同一個人&lt;/td>
 &lt;td>兩筆寫入重疊時誰贏、輸的那筆有沒有回報&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界&lt;/td>
 &lt;td>角色數為零、或一次帶入上限數量的角色&lt;/td>
 &lt;td>降到零角色的同時他正在送出一個需要權限的請求&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗&lt;/td>
 &lt;td>權限服務逾時、或只寫成功一半&lt;/td>
 &lt;td>失敗發生在寫入之後、快取失效通知之前&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>換一個流程，六個維度的內容全部要重填——這張表的欄位是問題，不是答案。前四個維度在任何有狀態的系統裡都推得出來；&lt;strong>併發與失敗兩個維度值得單獨說一句&lt;/strong>：它們在驗收條件層能寫到的程度比其他四個低，因為驗收多半跑在單執行緒的腳本裡。可行的寫法是把它們降級成「這個交叉發生時使用者該觀察到什麼」的陳述（輸的那筆要收到明確拒絕、而不是靜默覆蓋），把實際的競態驗證留給整合層。&lt;/p>
&lt;p>&lt;strong>這張表自己有一個盲區，而且是本章方法論的遞歸：空格只存在於已經被列出的維度上，漏掉的維度連格子都沒有。&lt;/strong> 找空格證明不了維度列得夠，所以維度清單要有第二個來源——最便宜的是既有的事故與客訴紀錄：逐則問它落在哪個維度，落不進表上任何一格的就是漏掉的那一個。沒有事故史時退而求其次，把表拿給另一個熟悉這個領域、但沒有參與寫條件的人補一輪。&lt;/p>
&lt;p>做法是逐條驗收條件標出它涵蓋哪個維度，再看維度兩兩相乘的格子裡哪些是空的——兩兩相乘是第一輪，更高維的交叉怎麼處理見下一節。空格不必全補——判斷依據是那個交叉發生時&lt;strong>消費者觀察得到的行為&lt;/strong>會不會不同，會不同就是需求缺口。答「觀察不到」的人要指出那個差異落在可觀察面清單（API 契約、時序、資源使用、錯誤形態、分佈不變量）之外的哪一側；&lt;strong>指不出來、或說不出從哪個介面觀察，就當成需求缺口補條件&lt;/strong>——誤補的代價是多一條條件，漏補的代價是那個維度永遠不會再被問起。&lt;/p>
&lt;p>消費者不是人的時候這一問要換個問法。下游是程式（函式庫、SDK、資料管線、內部服務）時，可觀察面是 API 契約加上時序、資源使用與錯誤形態——「換一種資料結構」在延遲或記憶體屬於契約的一部分時就不是自由度。下游是統計性流程（模型訓練、報表聚合）時，單筆記錄的差異可能觀察不到而分佈的差異觀察得到，判準要換成&lt;strong>分佈層的不變量&lt;/strong>（筆數、空值率、值域、欄位相關性），而不是逐筆的觀察。這一節的判準與上一段的預設是同一條：指不出可觀察的介面就補條件。處置的形態沿用&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">等價突變的時間盒紀律&lt;/a>——給判定一個時限，到了就落在有成本的那一側。&lt;/p>
&lt;h3 id="攤到幾維才停">攤到幾維才停&lt;/h3>
&lt;p>兩兩相乘是起點不是終點。真實系統裡最貴的缺陷常常住在三維以上——年約 × 期末 × 付款失敗、登入中 × 降權 × 快取尚未失效，這類組合在二維表上一格都不會出現。而三維的組合數是二維的好幾倍，全補是不可能的。&lt;/p>
&lt;p>停止的判準不是維數，是&lt;strong>代價&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>把每一個空著的交叉標一個「它發生時消費者損失多少」——金額、資料正確性、無法回復的操作、對外承諾的違反，取那個系統自己的單位。&lt;/li>
&lt;li>由大到小排序，從頂端往下補。&lt;/li>
&lt;li>補到「再補一條的成本超過那個交叉的代價」為止，然後停。停下來的位置要寫下來——那是這組條件的射程宣告，不是遺漏。&lt;/li>
&lt;/ol>
&lt;p>這條判準對維數是中立的：某個三維交叉的代價比所有二維交叉都高時它排在最前面，而多數四維組合會自動落到門檻以下。實務上代價的估算不需要精確，只需要能排序——「這個會讓使用者被多收一期的錢」與「這個會讓錯誤訊息的措辭不一致」之間的差距，不必量化就分得出來。&lt;/p>
&lt;p>代價估不出來的交叉另外處理：那通常代表這個情境的業務後果從來沒有人想過，而那件事本身比補一條條件更值得先解決。&lt;/p>
&lt;p>這個盤點的副產物往往比測試本身有價值：空格代表需求文件對那個情境沒有交代，而沒有交代的地方通常是產品決策從來沒有被做過的地方。權限異動這個例子裡，「降權時那張 session 怎麼算」多半不在任何文件裡，而它決定了使用者會不會在操作到一半突然被踢掉。&lt;/p>
&lt;h2 id="補條件的順序">補條件的順序&lt;/h2>
&lt;p>&lt;strong>先補改變外部行為的交叉。&lt;/strong> 判定順序、狀態轉換的合法性、失敗發生在流程中段時的補償，這些交叉一旦錯了使用者會看見。&lt;/p>
&lt;p>&lt;strong>再補契約性的承諾。&lt;/strong> 這一類是「文件上寫了、而沒有任何一條條件在守」的承諾。付款端點宣稱重試安全，就要有一條條件用同一個冪等鍵送兩次並斷言只扣一次款；分頁 API 宣稱排序穩定，就要有一條條件在兩次查詢之間插入一筆資料並斷言既有頁次的內容不位移。T.C10 裡的 &lt;code>--seed&lt;/code> 是同一類的最小形態——宣稱同樣輸入得到同樣輸出，而八個版本用了三種不同的亂數產生器，沒有一條條件發現。&lt;/p>
&lt;p>&lt;strong>終止與資源釋放要有自己的條件。&lt;/strong> 「跑得完」與「跑對」是兩件事，而多數驗收腳本只斷言後者。長時間執行的流程要斷言它會結束、結束時釋放了什麼。&lt;/p>
&lt;p>&lt;strong>最後才是同一維度上的密度。&lt;/strong> 邊界值列表有它的價值，但它排在交叉之後。&lt;/p>
&lt;h2 id="把通過的產出當成樣本">把通過的產出當成樣本&lt;/h2>
&lt;p>同一組條件會被重跑時（重寫、重構、換一個 agent 實作），要能說出這次的產出跟上次是不是同一類。做法是記下行為指紋而不只是「通過了」——原始碼的 checksum 是最粗的一種，更有用的是針對那幾個關鍵維度各跑一次探測（同房間兩個危險時觸發哪一個、同一個 seed 兩次執行結果是否相同、流程中斷後資料停在哪個狀態）。&lt;/p>
&lt;p>指紋改變而條件仍然通過，就是等價類裡的移動；此時要問移動落在哪個維度、那個維度該不該被規定。這比「重跑一次看有沒有過」多帶回一個資訊：&lt;strong>產出在條件的射程之外變了什麼。&lt;/strong>&lt;/p>
&lt;h2 id="邊界等價類大是正常的">邊界：等價類大是正常的&lt;/h2>
&lt;p>需求本來就只該約束外部行為，內部實作留給實作是設計上的正確選擇。等價類大本身不是缺陷，甚至是好的——條件把每一行程式碼都釘死的話，重構會變成不可能。&lt;/p>
&lt;p>要區分的是集合裡的差異落在哪一側：換一個變數名、把三個函式併成一個是自由度；換一個判定順序、換一種終止方式、換一個亂數來源，是需求沒寫。「換一種資料結構」要看情況——延遲或記憶體用量屬於契約時它就不是自由度。判別的問題是這個差異消費者觀察不觀察得到，而消費者是誰、可觀察面包含哪些維度，要先照前面那一節的方式定下來。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「驗收全過，可以交付」&lt;/td>
 &lt;td>先問這組條件切出的等價類有多大，把維度表攤開看空格&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補測試時一直在同一個邊界上加數值&lt;/td>
 &lt;td>該維度已經飽和，換方向補交叉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>線上出事而「測試裡有類似的案例」&lt;/td>
 &lt;td>類似不等於交叉，去看那個情境是兩個維度相乘的結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一份需求重跑產出的程式差很多，而全部通過&lt;/td>
 &lt;td>記行為指紋、指出差異落在哪個維度、判斷那個維度該不該被規定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗收條件讀起來已經把功能講完了&lt;/td>
 &lt;td>這個感覺不可靠，逐條標維度比通讀可靠&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維度表出現空格而沒有人知道該填什麼&lt;/td>
 &lt;td>那是產品決策從未被做過的地方，先把決策做出來再寫條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>契約性承諾（seed、冪等、排序穩定）只寫在文件裡&lt;/td>
 &lt;td>補成斷言，否則它不是承諾只是說法&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>條件由誰寫、為什麼不能與實作同源 → &lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源&lt;/a>&lt;/li>
&lt;li>條件寫好之後怎麼量測試的偵測能力 → &lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替&lt;/a>&lt;/li>
&lt;li>這個判準抽出的可重用原則 → &lt;a href="https://tarrragon.github.io/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277 通過關卡不等於通過的是同一個程式&lt;/a>&lt;/li>
&lt;li>實際量出等價類的案例 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10&lt;/a>&lt;/li>
&lt;li>條件下沉到協議層之後怎麼寫成契約 → &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/http-contract-test/" data-link-title="HTTP contract test 設計" data-link-desc="HTTP REST API 的 protocol integration test — request/response 格式、status code 語意、error body 結構的驗證">HTTP contract test 設計&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>一組驗收條件通過，證明的是產出落在該條件切出的等價類裡（這裡的等價類指<strong>能通過同一組條件的所有程式</strong>所成的集合，跟測試設計裡的等價類劃分不是同一件事——那個切的是輸入域，這個切的是程式）。等價類是所有能通過這組條件的程式所成的集合，它的大小由條件之間的<strong>交叉密度</strong>決定，不由條件的數量決定。設計驗收條件的工作因此不是「把功能講完」，而是把等價類縮到只剩下真正屬於實作自由度的差異。</p>
<p>閱讀退出之後，「那兩個同時發生怎麼辦」這句話沒有人會順口問了——它原本由讀程式碼的人隨手補上，現在要由條件的設計自己回答。</p>
<h2 id="等價類可以被量出來">等價類可以被量出來</h2>
<p><a href="/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10</a> 把這件事實際量了一次。產品是 Hunt the Wumpus，一個主控台文字冒險遊戲：玩家在洞穴間移動、房間裡可能有怪物、陷阱或蝙蝠。同一份需求從空目錄寫八次，八個程式全部通過同一組 25 案例的驗收，八棵原始碼樹沒有任何兩棵相同，行數從 331 到 603。</p>
<p>被放行的差異裡，大部分屬於實作自由度——檔案怎麼切、函式怎麼命名、內部用什麼資料結構，這些本來就該留給實作。但有三類不是：</p>
<p><strong>判定順序。</strong> 原規則是先檢查怪物、再陷阱、再蝙蝠。八個版本裡四個照做、三個反過來先檢查蝙蝠、一個的紀錄沒寫順序——兩種順序都通過，因為沒有任何一條驗收案例把兩個危險放進同一個房間。</p>
<p><strong>終止方式。</strong> 一個版本的主迴圈永不結束，一個外層迴圈永不離開，一個以拋出並吞掉例外的方式收場。驗收沒有斷言離開的方式。</p>
<p><strong>亂數來源。</strong> 三種不同的產生器，意味著同一個 seed 參數在不同版本產出不同結果——<code>--seed</code> 沒有成為可攜的契約。</p>
<p>這三類都是需求的一部分，只是沒有被寫進條件裡。<strong>它們與實作自由度的差別在於：改變它們會改變使用者觀察得到的行為。</strong></p>
<h2 id="密度與涵蓋面是兩件事">密度與涵蓋面是兩件事</h2>
<p><strong>同一維度上多加一條斷言，縮小等價類的幅度隨密度遞減；而對一個沒有任何條件碰過的維度，增量是零。</strong> 這條判準的操作含義是補到第十個邊界值的收益遠低於補第一條「兩件事同時發生」。</p>
<p>同一個實驗提供了它的外顯形態：八份程式的單元測試規模從零到 206 個範例，驗收結果卻完全一致——那 25 條驗收條件把每個危險單獨出現時的反應驗了很多遍，兩個危險同時出現一次都沒有，所以判定順序這個維度的增量始終是零。</p>
<p>它同時解釋了一種常見的挫折感——測試一直加、線上還是出事，而出的事「測試裡明明有類似的案例」。類似不等於交叉：同一個維度上的第十個案例，對另一個維度的貢獻仍然是零。</p>
<p>本章引用的量測數字全部來自同一份實驗，它撐得起與撐不起什麼寫在<a href="/blog/testing/06-agent-authored-code/" data-link-title="模組六：Agent 產出程式碼的驗證" data-link-desc="程式碼由 agent 寫、人不再逐行讀時，用來決定驗證該落在哪些位置，以及每個位置的射程到哪裡">模組入口</a>。</p>
<h2 id="量射程的操作方式">量射程的操作方式</h2>
<p>把條件攤成維度表，看哪些維度組合沒有被任何一條條件同時碰到。維度自己要先推出來，推法是逐一問「這個系統的行為會隨著什麼而不同」——答案裡每一個獨立的自變數就是一個維度。</p>
<p>下面用一個具名流程走一次，讓推導的結果看得見：<strong>帳號權限異動</strong>（管理者調整某位使用者的角色，而該使用者可能正在使用系統）。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>這個流程裡它是什麼</th>
          <th>交叉的形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>狀態</td>
          <td>使用者當下是登入中、閒置、還是已停用</td>
          <td>對一個登入中的使用者降權，他手上那張 session 怎麼算</td>
      </tr>
      <tr>
          <td>事件</td>
          <td>收到的是升權、降權、還是整組角色置換</td>
          <td>升權與降權在同一個時間窗內各來一次</td>
      </tr>
      <tr>
          <td>順序</td>
          <td>兩次異動的先後</td>
          <td>先降後升與先升後降的最終權限該不該相同</td>
      </tr>
      <tr>
          <td>併發</td>
          <td>兩位管理者同時改同一個人</td>
          <td>兩筆寫入重疊時誰贏、輸的那筆有沒有回報</td>
      </tr>
      <tr>
          <td>邊界</td>
          <td>角色數為零、或一次帶入上限數量的角色</td>
          <td>降到零角色的同時他正在送出一個需要權限的請求</td>
      </tr>
      <tr>
          <td>失敗</td>
          <td>權限服務逾時、或只寫成功一半</td>
          <td>失敗發生在寫入之後、快取失效通知之前</td>
      </tr>
  </tbody>
</table>
<p>換一個流程，六個維度的內容全部要重填——這張表的欄位是問題，不是答案。前四個維度在任何有狀態的系統裡都推得出來；<strong>併發與失敗兩個維度值得單獨說一句</strong>：它們在驗收條件層能寫到的程度比其他四個低，因為驗收多半跑在單執行緒的腳本裡。可行的寫法是把它們降級成「這個交叉發生時使用者該觀察到什麼」的陳述（輸的那筆要收到明確拒絕、而不是靜默覆蓋），把實際的競態驗證留給整合層。</p>
<p><strong>這張表自己有一個盲區，而且是本章方法論的遞歸：空格只存在於已經被列出的維度上，漏掉的維度連格子都沒有。</strong> 找空格證明不了維度列得夠，所以維度清單要有第二個來源——最便宜的是既有的事故與客訴紀錄：逐則問它落在哪個維度，落不進表上任何一格的就是漏掉的那一個。沒有事故史時退而求其次，把表拿給另一個熟悉這個領域、但沒有參與寫條件的人補一輪。</p>
<p>做法是逐條驗收條件標出它涵蓋哪個維度，再看維度兩兩相乘的格子裡哪些是空的——兩兩相乘是第一輪，更高維的交叉怎麼處理見下一節。空格不必全補——判斷依據是那個交叉發生時<strong>消費者觀察得到的行為</strong>會不會不同，會不同就是需求缺口。答「觀察不到」的人要指出那個差異落在可觀察面清單（API 契約、時序、資源使用、錯誤形態、分佈不變量）之外的哪一側；<strong>指不出來、或說不出從哪個介面觀察，就當成需求缺口補條件</strong>——誤補的代價是多一條條件，漏補的代價是那個維度永遠不會再被問起。</p>
<p>消費者不是人的時候這一問要換個問法。下游是程式（函式庫、SDK、資料管線、內部服務）時，可觀察面是 API 契約加上時序、資源使用與錯誤形態——「換一種資料結構」在延遲或記憶體屬於契約的一部分時就不是自由度。下游是統計性流程（模型訓練、報表聚合）時，單筆記錄的差異可能觀察不到而分佈的差異觀察得到，判準要換成<strong>分佈層的不變量</strong>（筆數、空值率、值域、欄位相關性），而不是逐筆的觀察。這一節的判準與上一段的預設是同一條：指不出可觀察的介面就補條件。處置的形態沿用<a href="/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">等價突變的時間盒紀律</a>——給判定一個時限，到了就落在有成本的那一側。</p>
<h3 id="攤到幾維才停">攤到幾維才停</h3>
<p>兩兩相乘是起點不是終點。真實系統裡最貴的缺陷常常住在三維以上——年約 × 期末 × 付款失敗、登入中 × 降權 × 快取尚未失效，這類組合在二維表上一格都不會出現。而三維的組合數是二維的好幾倍，全補是不可能的。</p>
<p>停止的判準不是維數，是<strong>代價</strong>：</p>
<ol>
<li>把每一個空著的交叉標一個「它發生時消費者損失多少」——金額、資料正確性、無法回復的操作、對外承諾的違反，取那個系統自己的單位。</li>
<li>由大到小排序，從頂端往下補。</li>
<li>補到「再補一條的成本超過那個交叉的代價」為止，然後停。停下來的位置要寫下來——那是這組條件的射程宣告，不是遺漏。</li>
</ol>
<p>這條判準對維數是中立的：某個三維交叉的代價比所有二維交叉都高時它排在最前面，而多數四維組合會自動落到門檻以下。實務上代價的估算不需要精確，只需要能排序——「這個會讓使用者被多收一期的錢」與「這個會讓錯誤訊息的措辭不一致」之間的差距，不必量化就分得出來。</p>
<p>代價估不出來的交叉另外處理：那通常代表這個情境的業務後果從來沒有人想過，而那件事本身比補一條條件更值得先解決。</p>
<p>這個盤點的副產物往往比測試本身有價值：空格代表需求文件對那個情境沒有交代，而沒有交代的地方通常是產品決策從來沒有被做過的地方。權限異動這個例子裡，「降權時那張 session 怎麼算」多半不在任何文件裡，而它決定了使用者會不會在操作到一半突然被踢掉。</p>
<h2 id="補條件的順序">補條件的順序</h2>
<p><strong>先補改變外部行為的交叉。</strong> 判定順序、狀態轉換的合法性、失敗發生在流程中段時的補償，這些交叉一旦錯了使用者會看見。</p>
<p><strong>再補契約性的承諾。</strong> 這一類是「文件上寫了、而沒有任何一條條件在守」的承諾。付款端點宣稱重試安全，就要有一條條件用同一個冪等鍵送兩次並斷言只扣一次款；分頁 API 宣稱排序穩定，就要有一條條件在兩次查詢之間插入一筆資料並斷言既有頁次的內容不位移。T.C10 裡的 <code>--seed</code> 是同一類的最小形態——宣稱同樣輸入得到同樣輸出，而八個版本用了三種不同的亂數產生器，沒有一條條件發現。</p>
<p><strong>終止與資源釋放要有自己的條件。</strong> 「跑得完」與「跑對」是兩件事，而多數驗收腳本只斷言後者。長時間執行的流程要斷言它會結束、結束時釋放了什麼。</p>
<p><strong>最後才是同一維度上的密度。</strong> 邊界值列表有它的價值，但它排在交叉之後。</p>
<h2 id="把通過的產出當成樣本">把通過的產出當成樣本</h2>
<p>同一組條件會被重跑時（重寫、重構、換一個 agent 實作），要能說出這次的產出跟上次是不是同一類。做法是記下行為指紋而不只是「通過了」——原始碼的 checksum 是最粗的一種，更有用的是針對那幾個關鍵維度各跑一次探測（同房間兩個危險時觸發哪一個、同一個 seed 兩次執行結果是否相同、流程中斷後資料停在哪個狀態）。</p>
<p>指紋改變而條件仍然通過，就是等價類裡的移動；此時要問移動落在哪個維度、那個維度該不該被規定。這比「重跑一次看有沒有過」多帶回一個資訊：<strong>產出在條件的射程之外變了什麼。</strong></p>
<h2 id="邊界等價類大是正常的">邊界：等價類大是正常的</h2>
<p>需求本來就只該約束外部行為，內部實作留給實作是設計上的正確選擇。等價類大本身不是缺陷，甚至是好的——條件把每一行程式碼都釘死的話，重構會變成不可能。</p>
<p>要區分的是集合裡的差異落在哪一側：換一個變數名、把三個函式併成一個是自由度；換一個判定順序、換一種終止方式、換一個亂數來源，是需求沒寫。「換一種資料結構」要看情況——延遲或記憶體用量屬於契約時它就不是自由度。判別的問題是這個差異消費者觀察不觀察得到，而消費者是誰、可觀察面包含哪些維度，要先照前面那一節的方式定下來。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「驗收全過，可以交付」</td>
          <td>先問這組條件切出的等價類有多大，把維度表攤開看空格</td>
      </tr>
      <tr>
          <td>補測試時一直在同一個邊界上加數值</td>
          <td>該維度已經飽和，換方向補交叉</td>
      </tr>
      <tr>
          <td>線上出事而「測試裡有類似的案例」</td>
          <td>類似不等於交叉，去看那個情境是兩個維度相乘的結果</td>
      </tr>
      <tr>
          <td>同一份需求重跑產出的程式差很多，而全部通過</td>
          <td>記行為指紋、指出差異落在哪個維度、判斷那個維度該不該被規定</td>
      </tr>
      <tr>
          <td>驗收條件讀起來已經把功能講完了</td>
          <td>這個感覺不可靠，逐條標維度比通讀可靠</td>
      </tr>
      <tr>
          <td>維度表出現空格而沒有人知道該填什麼</td>
          <td>那是產品決策從未被做過的地方，先把決策做出來再寫條件</td>
      </tr>
      <tr>
          <td>契約性承諾（seed、冪等、排序穩定）只寫在文件裡</td>
          <td>補成斷言，否則它不是承諾只是說法</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>條件由誰寫、為什麼不能與實作同源 → <a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a></li>
<li>條件寫好之後怎麼量測試的偵測能力 → <a href="/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替</a></li>
<li>這個判準抽出的可重用原則 → <a href="/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277 通過關卡不等於通過的是同一個程式</a></li>
<li>實際量出等價類的案例 → <a href="/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10</a></li>
<li>條件下沉到協議層之後怎麼寫成契約 → <a href="/blog/testing/03-protocol-integration-test/http-contract-test/" data-link-title="HTTP contract test 設計" data-link-desc="HTTP REST API 的 protocol integration test — request/response 格式、status code 語意、error body 結構的驗證">HTTP contract test 設計</a></li>
</ul>
]]></content:encoded></item><item><title>品質閘門的更替：覆蓋率、突變分數與各自的射程</title><link>https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/</guid><description>&lt;p>品質閘門要量的是測試的偵測能力：程式在某個位置改掉行為時，這套測試會不會變紅。行覆蓋率量的是執行足跡，它與偵測能力之間隔著一個假設——執行到了通常也就斷言了。&lt;/p>
&lt;p>這個假設從來沒有完全成立。站內的 &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問&lt;/a>把 &lt;code>isNotNull&lt;/code>、&lt;code>isA&lt;/code> 列為人手寫測試裡&lt;strong>常見&lt;/strong>的無效斷言，&lt;a href="https://tarrragon.github.io/blog/testing/cases/ansi-parser-test-data-blindspot/" data-link-title="T.C3 ANSI parser 測試資料不覆蓋真實 shell output" data-link-desc="ANSI parser 只處理基本 SGR 色彩碼、unit test 用手寫乾淨字串驗證 — 真實 zsh prompt 送出 OSC 標題設定、CSI private mode 游標隱藏、括號貼上模式等數十種控制序列，全部殘留為亂碼">T.C3&lt;/a> 就是一個實例。變的是規模：套件小到可以逐條抽查時，這類斷言會在 review 裡被指出來；斷言可以整批產生之後抽查追不上，塌掉的是這個假設的可稽查性——它本身的正確性一直都只是近似。於是需要一個不靠抽查、而且用空斷言達不到的指標——&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">突變測試&lt;/a>是目前唯一一種空斷言達不到的成熟指標——這個性質來自它的構造，不隨工具生態變動。&lt;/p>
&lt;h2 id="覆蓋率失效的形狀">覆蓋率失效的形狀&lt;/h2>
&lt;p>覆蓋率不是變得不準，是執行足跡與偵測能力之間的相關性斷了。一個測試呼叫函式、檢查它沒有拋出例外，覆蓋率把那幾行算進去；同一個測試對回傳值不做任何檢查，偵測能力是零。這種測試被稱為永遠綠的測試，它在覆蓋率報表上與一個嚴謹的測試無法區分。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10&lt;/a> 的數據顯示了這個斷裂的程度：事後補測試、逐函式測試、以及 TDD 三法則（沒有失敗的測試就不寫 production code、只寫剛好會失敗的測試、只寫剛好讓它通過的實作）加上複雜度上限，這三種做法產出的程式與套件組成完全不同，行覆蓋率全部停在 97% 到 99%。被覆蓋率區分出來的只有兩端——零測試又不開複雜度上限那一版停在 14% 與 24%（前者是分支形式的覆蓋、後者是行覆蓋；同一組紀律開啟複雜度上限的那一列被迫生出 40 個範例、覆蓋率跟著跳上來），數字全部來自載入而非斷言；三法則不開複雜度上限的那一版停在 85.81% 行覆蓋，作者標為 outlier，成因是主控台迴圈只被輕度驅動。也就是說它分得出「有沒有測試」與「有沒有一整塊沒被驅動」，分不出「測試好不好」。分得出有無、分不出好壞。&lt;/p>
&lt;p>這給覆蓋率一個誠實的定位：它是下限指標。低覆蓋率確實代表有問題，高覆蓋率不代表沒問題，而把它當成通過條件會讓「達成它」變成最便宜路徑的目標。&lt;/p>
&lt;h2 id="突變分數量的是什麼">突變分數量的是什麼&lt;/h2>
&lt;p>突變測試對程式注入單點的行為改變——把 &lt;code>&amp;gt;&lt;/code> 換成 &lt;code>&amp;gt;=&lt;/code>、把回傳值換成常數、刪掉一次呼叫——再看測試會不會因此變紅。變紅稱為殺死，全綠稱為存活。存活的突變指出一件具體的事：程式在那個位置改掉行為，這套測試不會發現。&lt;/p>
&lt;p>突變分數（殺死數除以有效突變數）用&lt;strong>字面上的空斷言&lt;/strong>達不到：要殺死突變，斷言必須真的檢查到那個行為。這是它取代覆蓋率當閘門的理由——不是它更精確，是覆蓋率最便宜的那條達成路徑對它無效。&lt;/p>
&lt;p>但「低思考量」的路徑沒有被完全堵死，下一段的四條就是它們，其中兩條的思考量與空斷言同級。所以突變分數擋掉的是一種特定的作弊，不是所有作弊。&lt;/p>
&lt;p>它並沒有因此免疫於&lt;a href="https://tarrragon.github.io/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">機械約束的通病&lt;/a>。這件事在分數只是本機參考值時不會發生，一旦它變成 CI 的通過門檻或團隊報表上的一個數字，讓它上升就有了立即收益——而有立即收益的最便宜動作一定會發生。便宜的達成路徑至少有四條。前三條改在設定檔裡——縮小掃描範圍（只掃已經寫好測試的模組）、放寬等價突變的排除清單、削弱算子集（拿掉最會存活的那幾種突變）；它們不留痕跡於測試碼，所以設定檔要進版控，變更要跟分數變化一起看。&lt;/p>
&lt;p>第四條在測試碼裡，而且它是這裡最需要提防的一條：&lt;strong>錄製式斷言&lt;/strong>。把整個回傳值序列化後跟一份存下來的快照比對，思考量跟空斷言同級（不必想清楚哪個欄位重要），卻能一次殺死大量突變。它的判準是現狀而不是需求，也就是把整套測試變成&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">行為快照&lt;/a>——分數很高而「這個行為對不對」一個字都沒回答。防它的方式不是禁用快照，是問&lt;strong>這份快照的預期值是誰決定的&lt;/strong>：由人審過的規格產生的合法，由實作跑一次產生的不合法。&lt;/p>
&lt;p>還有一個不算作弊、但會讓分數失真的來源：&lt;strong>例外導向的殺死&lt;/strong>。造成崩潰的突變連零斷言的 smoke test 都會紅，所以一套幾乎不做檢查的測試在多數專案也拿得到兩三成分數。看絕對分數之前先看這個基線。&lt;/p>
&lt;p>導入的成本結構跟其他測試層不同：每個突變都要重跑一次相關測試，總時間是突變數乘上測試時間。壓下來靠三件事：&lt;/p>
&lt;p>&lt;strong>只跑這次改動的檔案。&lt;/strong> 完整跑一次的代價通常只在夜間排程或發版前接受得了，日常回饋要用差異模式。&lt;/p>
&lt;p>&lt;strong>用覆蓋率資料跳過沒被執行到的突變。&lt;/strong> 沒有任何測試執行到的程式碼，突變一定存活，跑它只是浪費——這是覆蓋率在新配置下仍然有用的地方：它變成突變測試的輸入而不是閘門本身。&lt;/p>
&lt;p>&lt;strong>平行執行並設定逾時。&lt;/strong> 有些突變會造成無窮迴圈，逾時要設得夠短，否則單一突變就吃掉整輪時間。&lt;/p>
&lt;p>覆蓋率與偵測能力脫鉤這件事另有站內既有內容佐證（本章開頭引的 Assertion 品質三問與 T.C3），不單靠那份實驗；實驗本身的射程見&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/" data-link-title="模組六：Agent 產出程式碼的驗證" data-link-desc="程式碼由 agent 寫、人不再逐行讀時，用來決定驗證該落在哪些位置，以及每個位置的射程到哪裡">模組入口&lt;/a>。&lt;/p>
&lt;h2 id="存活的突變分兩類">存活的突變分兩類&lt;/h2>
&lt;p>處理方式相反，判錯了會傷到測試套件。&lt;/p>
&lt;p>&lt;strong>真的漏洞。&lt;/strong> 那個位置的行為改變確實沒有被任何斷言檢查到，補一條斷言。這是多數情形。&lt;/p>
&lt;p>&lt;strong>等價突變。&lt;/strong> 改寫之後行為完全相同。典型形態有兩種：一是等價的邊界寫法（&lt;code>i &amp;lt; n&lt;/code> 換成 &lt;code>i &amp;lt;= n - 1&lt;/code>），二是對結果沒有影響的短路順序（兩個都是純函式的條件互換前後）。這是工具的限制，要標記排除。為等價突變硬補斷言的代價是那條斷言必然綁在實作的寫法上，反過來傷害重構安全性，與&lt;a href="https://tarrragon.github.io/blog/record/behavior-first-tdd-methodology/" data-link-title="行為優先的 TDD：測試耦合行為、不耦合結構" data-link-desc="重構時測試大量壞掉、或在「該不該 mock 這個協作者」上需要一個可執行的立場時，本站對兩派 TDD 分歧的選擇與推導">行為優先的 TDD&lt;/a> 要守的性質直接衝突。&lt;/p>
&lt;p>判斷方式是問這個突變如果留在正式版本，有沒有任何一種輸入會讓使用者觀察到不同的結果。答案是沒有，它就是等價突變。&lt;/p>
&lt;p>這個問題形式上可執行、實務上判不出來（等價性一般而言不可判定），所以要配一套不靠判準的處置紀律：單一突變的判定給一個時間盒（十五分鐘是個起始值，依領域邏輯的密度自行調整——密集的規則引擎值得久一點，膠水層更短），時間盒到了預設歸類為&lt;strong>真漏洞&lt;/strong>而非等價突變（誤判成漏洞的代價是多寫一條斷言，誤判成等價的代價是永久失去一個偵測點）、排除清單的每一條要寫下「想不出區分輸入」的理由並具名，且由第二個人覆核。單人維護的專案沒有第二個人，這一項退化成定期回看自己的排除清單——此時清單的成長速度是唯一還在運作的訊號——它比程式碼成長得快時，判定已經放水了。&lt;/p>
&lt;h2 id="突變分數守不住什麼">突變分數守不住什麼&lt;/h2>
&lt;p>這是導入時最需要先講清楚的一件事：&lt;strong>突變是對照著程式現在的樣子產生的&lt;/strong>，所以它守得住「斷言不夠密」，守不住「這段程式一開始就實作錯了需求」。&lt;/p>
&lt;p>把危險判定順序寫反的那份程式跑突變測試，長出來的斷言會把那個順序釘得更牢（機制推論，非實驗觀察）。那份實驗實際記載的是：突變長出來的第二套測試補的全是運算子層的翻轉，作者的結論是它「沒有發現一個不同的遊戲」。&lt;/p>
&lt;p>這個限制在測試與實作&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">出處不獨立&lt;/a>時特別要緊，因為那正是規格層錯誤最容易產生的情境。突變分數 100% 的套件仍然可以整套建立在一個錯誤的需求理解上。&lt;/p>
&lt;h2 id="閘門要成對設計">閘門要成對設計&lt;/h2>
&lt;p>每個機械閘門都有一個它量不到的維度，而達成該閘門最便宜的路徑通常就走在那個維度上。配對的檢查點要一起設，否則閘門會被最便宜的方式滿足。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>閘門&lt;/th>
 &lt;th>它量得到&lt;/th>
 &lt;th>它量不到&lt;/th>
 &lt;th>配對要補的那件事（只有第一列可自動化，其餘三列是人的判準設計）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>行覆蓋率&lt;/td>
 &lt;td>執行足跡&lt;/td>
 &lt;td>斷言強度&lt;/td>
 &lt;td>突變分數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>突變分數&lt;/td>
 &lt;td>對現有程式的偵測能力&lt;/td>
 &lt;td>需求是否被正確實作&lt;/td>
 &lt;td>攤開條件的維度表找沒有被碰過的交叉（&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類&lt;/a>），並確認條件的出處與實作獨立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>複雜度上限&lt;/td>
 &lt;td>單一函式的分支數&lt;/td>
 &lt;td>拆出來的名字還指不指涉一個完整的領域概念&lt;/td>
 &lt;td>逐個新名字問它對應到哪個業務動作，對不到的是切碎&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試數量&lt;/td>
 &lt;td>套件規模&lt;/td>
 &lt;td>涵蓋了幾個維度&lt;/td>
 &lt;td>攤開維度表數空格（&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類&lt;/a>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/cyclomatic-complexity/" data-link-title="Cyclomatic Complexity（圈複雜度）" data-link-desc="看到函式長度或複雜度上限這類數字門檻時，用來判斷它量的是什麼、以及達成它的兩條路徑為什麼在指標上分不出來">複雜度上限&lt;/a>那一列有實測的代價可以參考：同一份實驗在程式寫完後強制把圈複雜度壓到 3 以下，四個版本的函式數大致翻倍、覆蓋率上升到 97% 以上，而原作者給的可讀性評分全部落在最低的兩級，設計評分一次都沒有上升。他的描述是這個約束「不做簡化，只是繁殖名字」。這條的完整推導在 &lt;a href="https://tarrragon.github.io/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278&lt;/a>。&lt;/p>
&lt;h2 id="兩個前置條件">兩個前置條件&lt;/h2>
&lt;p>這一章的作法有兩個硬前提，任一個不成立時整套跑不起來，而它們不會在文件裡自己現身。&lt;/p>
&lt;p>&lt;strong>測試套件要是確定性的。&lt;/strong> 突變測試對 flaky test 極度敏感：一個隨機紅燈會被記成「殺死突變」，分數因此變成噪音、而且是往上飄的噪音。導入之前要先把已知的不穩定測試&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/quarantine/" data-link-title="Quarantine（隔離）" data-link-desc="把已知 flaky 的測試從主要執行路徑移開、保留修復壓力的治理機制；與 skip 的語意差異在於 quarantine 有負責人和回收期限">隔離&lt;/a>出去，並確認隔離清單不在掃描範圍內。&lt;/p>
&lt;p>&lt;strong>該語言要有一個跑得動的突變工具。&lt;/strong> 工具的成熟度分語言差距很大（寫作當下 Java、JavaScript 與 Python 各有生產可用的實作，而包含 Dart 在內的不少語言還沒有），而這個生態逐年變動——導入前查一次該語言的現行實作，不要拿本文的舉例當現況。工具不存在時的替代品是&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">性質式測試&lt;/a>加上人工的斷言強度抽查，不是降級回覆蓋率。&lt;/p>
&lt;h2 id="導入順序">導入順序&lt;/h2>
&lt;p>先有可信的判準，再談分數。順序顛倒的話，得到的是一套把錯誤需求釘得很牢的高分測試。&lt;/p></description><content:encoded><![CDATA[<p>品質閘門要量的是測試的偵測能力：程式在某個位置改掉行為時，這套測試會不會變紅。行覆蓋率量的是執行足跡，它與偵測能力之間隔著一個假設——執行到了通常也就斷言了。</p>
<p>這個假設從來沒有完全成立。站內的 <a href="/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問</a>把 <code>isNotNull</code>、<code>isA</code> 列為人手寫測試裡<strong>常見</strong>的無效斷言，<a href="/blog/testing/cases/ansi-parser-test-data-blindspot/" data-link-title="T.C3 ANSI parser 測試資料不覆蓋真實 shell output" data-link-desc="ANSI parser 只處理基本 SGR 色彩碼、unit test 用手寫乾淨字串驗證 — 真實 zsh prompt 送出 OSC 標題設定、CSI private mode 游標隱藏、括號貼上模式等數十種控制序列，全部殘留為亂碼">T.C3</a> 就是一個實例。變的是規模：套件小到可以逐條抽查時，這類斷言會在 review 裡被指出來；斷言可以整批產生之後抽查追不上，塌掉的是這個假設的可稽查性——它本身的正確性一直都只是近似。於是需要一個不靠抽查、而且用空斷言達不到的指標——<a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">突變測試</a>是目前唯一一種空斷言達不到的成熟指標——這個性質來自它的構造，不隨工具生態變動。</p>
<h2 id="覆蓋率失效的形狀">覆蓋率失效的形狀</h2>
<p>覆蓋率不是變得不準，是執行足跡與偵測能力之間的相關性斷了。一個測試呼叫函式、檢查它沒有拋出例外，覆蓋率把那幾行算進去；同一個測試對回傳值不做任何檢查，偵測能力是零。這種測試被稱為永遠綠的測試，它在覆蓋率報表上與一個嚴謹的測試無法區分。</p>
<p><a href="/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10</a> 的數據顯示了這個斷裂的程度：事後補測試、逐函式測試、以及 TDD 三法則（沒有失敗的測試就不寫 production code、只寫剛好會失敗的測試、只寫剛好讓它通過的實作）加上複雜度上限，這三種做法產出的程式與套件組成完全不同，行覆蓋率全部停在 97% 到 99%。被覆蓋率區分出來的只有兩端——零測試又不開複雜度上限那一版停在 14% 與 24%（前者是分支形式的覆蓋、後者是行覆蓋；同一組紀律開啟複雜度上限的那一列被迫生出 40 個範例、覆蓋率跟著跳上來），數字全部來自載入而非斷言；三法則不開複雜度上限的那一版停在 85.81% 行覆蓋，作者標為 outlier，成因是主控台迴圈只被輕度驅動。也就是說它分得出「有沒有測試」與「有沒有一整塊沒被驅動」，分不出「測試好不好」。分得出有無、分不出好壞。</p>
<p>這給覆蓋率一個誠實的定位：它是下限指標。低覆蓋率確實代表有問題，高覆蓋率不代表沒問題，而把它當成通過條件會讓「達成它」變成最便宜路徑的目標。</p>
<h2 id="突變分數量的是什麼">突變分數量的是什麼</h2>
<p>突變測試對程式注入單點的行為改變——把 <code>&gt;</code> 換成 <code>&gt;=</code>、把回傳值換成常數、刪掉一次呼叫——再看測試會不會因此變紅。變紅稱為殺死，全綠稱為存活。存活的突變指出一件具體的事：程式在那個位置改掉行為，這套測試不會發現。</p>
<p>突變分數（殺死數除以有效突變數）用<strong>字面上的空斷言</strong>達不到：要殺死突變，斷言必須真的檢查到那個行為。這是它取代覆蓋率當閘門的理由——不是它更精確，是覆蓋率最便宜的那條達成路徑對它無效。</p>
<p>但「低思考量」的路徑沒有被完全堵死，下一段的四條就是它們，其中兩條的思考量與空斷言同級。所以突變分數擋掉的是一種特定的作弊，不是所有作弊。</p>
<p>它並沒有因此免疫於<a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">機械約束的通病</a>。這件事在分數只是本機參考值時不會發生，一旦它變成 CI 的通過門檻或團隊報表上的一個數字，讓它上升就有了立即收益——而有立即收益的最便宜動作一定會發生。便宜的達成路徑至少有四條。前三條改在設定檔裡——縮小掃描範圍（只掃已經寫好測試的模組）、放寬等價突變的排除清單、削弱算子集（拿掉最會存活的那幾種突變）；它們不留痕跡於測試碼，所以設定檔要進版控，變更要跟分數變化一起看。</p>
<p>第四條在測試碼裡，而且它是這裡最需要提防的一條：<strong>錄製式斷言</strong>。把整個回傳值序列化後跟一份存下來的快照比對，思考量跟空斷言同級（不必想清楚哪個欄位重要），卻能一次殺死大量突變。它的判準是現狀而不是需求，也就是把整套測試變成<a href="/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">行為快照</a>——分數很高而「這個行為對不對」一個字都沒回答。防它的方式不是禁用快照，是問<strong>這份快照的預期值是誰決定的</strong>：由人審過的規格產生的合法，由實作跑一次產生的不合法。</p>
<p>還有一個不算作弊、但會讓分數失真的來源：<strong>例外導向的殺死</strong>。造成崩潰的突變連零斷言的 smoke test 都會紅，所以一套幾乎不做檢查的測試在多數專案也拿得到兩三成分數。看絕對分數之前先看這個基線。</p>
<p>導入的成本結構跟其他測試層不同：每個突變都要重跑一次相關測試，總時間是突變數乘上測試時間。壓下來靠三件事：</p>
<p><strong>只跑這次改動的檔案。</strong> 完整跑一次的代價通常只在夜間排程或發版前接受得了，日常回饋要用差異模式。</p>
<p><strong>用覆蓋率資料跳過沒被執行到的突變。</strong> 沒有任何測試執行到的程式碼，突變一定存活，跑它只是浪費——這是覆蓋率在新配置下仍然有用的地方：它變成突變測試的輸入而不是閘門本身。</p>
<p><strong>平行執行並設定逾時。</strong> 有些突變會造成無窮迴圈，逾時要設得夠短，否則單一突變就吃掉整輪時間。</p>
<p>覆蓋率與偵測能力脫鉤這件事另有站內既有內容佐證（本章開頭引的 Assertion 品質三問與 T.C3），不單靠那份實驗；實驗本身的射程見<a href="/blog/testing/06-agent-authored-code/" data-link-title="模組六：Agent 產出程式碼的驗證" data-link-desc="程式碼由 agent 寫、人不再逐行讀時，用來決定驗證該落在哪些位置，以及每個位置的射程到哪裡">模組入口</a>。</p>
<h2 id="存活的突變分兩類">存活的突變分兩類</h2>
<p>處理方式相反，判錯了會傷到測試套件。</p>
<p><strong>真的漏洞。</strong> 那個位置的行為改變確實沒有被任何斷言檢查到，補一條斷言。這是多數情形。</p>
<p><strong>等價突變。</strong> 改寫之後行為完全相同。典型形態有兩種：一是等價的邊界寫法（<code>i &lt; n</code> 換成 <code>i &lt;= n - 1</code>），二是對結果沒有影響的短路順序（兩個都是純函式的條件互換前後）。這是工具的限制，要標記排除。為等價突變硬補斷言的代價是那條斷言必然綁在實作的寫法上，反過來傷害重構安全性，與<a href="/blog/record/behavior-first-tdd-methodology/" data-link-title="行為優先的 TDD：測試耦合行為、不耦合結構" data-link-desc="重構時測試大量壞掉、或在「該不該 mock 這個協作者」上需要一個可執行的立場時，本站對兩派 TDD 分歧的選擇與推導">行為優先的 TDD</a> 要守的性質直接衝突。</p>
<p>判斷方式是問這個突變如果留在正式版本，有沒有任何一種輸入會讓使用者觀察到不同的結果。答案是沒有，它就是等價突變。</p>
<p>這個問題形式上可執行、實務上判不出來（等價性一般而言不可判定），所以要配一套不靠判準的處置紀律：單一突變的判定給一個時間盒（十五分鐘是個起始值，依領域邏輯的密度自行調整——密集的規則引擎值得久一點，膠水層更短），時間盒到了預設歸類為<strong>真漏洞</strong>而非等價突變（誤判成漏洞的代價是多寫一條斷言，誤判成等價的代價是永久失去一個偵測點）、排除清單的每一條要寫下「想不出區分輸入」的理由並具名，且由第二個人覆核。單人維護的專案沒有第二個人，這一項退化成定期回看自己的排除清單——此時清單的成長速度是唯一還在運作的訊號——它比程式碼成長得快時，判定已經放水了。</p>
<h2 id="突變分數守不住什麼">突變分數守不住什麼</h2>
<p>這是導入時最需要先講清楚的一件事：<strong>突變是對照著程式現在的樣子產生的</strong>，所以它守得住「斷言不夠密」，守不住「這段程式一開始就實作錯了需求」。</p>
<p>把危險判定順序寫反的那份程式跑突變測試，長出來的斷言會把那個順序釘得更牢（機制推論，非實驗觀察）。那份實驗實際記載的是：突變長出來的第二套測試補的全是運算子層的翻轉，作者的結論是它「沒有發現一個不同的遊戲」。</p>
<p>這個限制在測試與實作<a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">出處不獨立</a>時特別要緊，因為那正是規格層錯誤最容易產生的情境。突變分數 100% 的套件仍然可以整套建立在一個錯誤的需求理解上。</p>
<h2 id="閘門要成對設計">閘門要成對設計</h2>
<p>每個機械閘門都有一個它量不到的維度，而達成該閘門最便宜的路徑通常就走在那個維度上。配對的檢查點要一起設，否則閘門會被最便宜的方式滿足。</p>
<table>
  <thead>
      <tr>
          <th>閘門</th>
          <th>它量得到</th>
          <th>它量不到</th>
          <th>配對要補的那件事（只有第一列可自動化，其餘三列是人的判準設計）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>行覆蓋率</td>
          <td>執行足跡</td>
          <td>斷言強度</td>
          <td>突變分數</td>
      </tr>
      <tr>
          <td>突變分數</td>
          <td>對現有程式的偵測能力</td>
          <td>需求是否被正確實作</td>
          <td>攤開條件的維度表找沒有被碰過的交叉（<a href="/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類</a>），並確認條件的出處與實作獨立</td>
      </tr>
      <tr>
          <td>複雜度上限</td>
          <td>單一函式的分支數</td>
          <td>拆出來的名字還指不指涉一個完整的領域概念</td>
          <td>逐個新名字問它對應到哪個業務動作，對不到的是切碎</td>
      </tr>
      <tr>
          <td>測試數量</td>
          <td>套件規模</td>
          <td>涵蓋了幾個維度</td>
          <td>攤開維度表數空格（<a href="/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類</a>）</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/testing/knowledge-cards/cyclomatic-complexity/" data-link-title="Cyclomatic Complexity（圈複雜度）" data-link-desc="看到函式長度或複雜度上限這類數字門檻時，用來判斷它量的是什麼、以及達成它的兩條路徑為什麼在指標上分不出來">複雜度上限</a>那一列有實測的代價可以參考：同一份實驗在程式寫完後強制把圈複雜度壓到 3 以下，四個版本的函式數大致翻倍、覆蓋率上升到 97% 以上，而原作者給的可讀性評分全部落在最低的兩級，設計評分一次都沒有上升。他的描述是這個約束「不做簡化，只是繁殖名字」。這條的完整推導在 <a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278</a>。</p>
<h2 id="兩個前置條件">兩個前置條件</h2>
<p>這一章的作法有兩個硬前提，任一個不成立時整套跑不起來，而它們不會在文件裡自己現身。</p>
<p><strong>測試套件要是確定性的。</strong> 突變測試對 flaky test 極度敏感：一個隨機紅燈會被記成「殺死突變」，分數因此變成噪音、而且是往上飄的噪音。導入之前要先把已知的不穩定測試<a href="/blog/testing/knowledge-cards/quarantine/" data-link-title="Quarantine（隔離）" data-link-desc="把已知 flaky 的測試從主要執行路徑移開、保留修復壓力的治理機制；與 skip 的語意差異在於 quarantine 有負責人和回收期限">隔離</a>出去，並確認隔離清單不在掃描範圍內。</p>
<p><strong>該語言要有一個跑得動的突變工具。</strong> 工具的成熟度分語言差距很大（寫作當下 Java、JavaScript 與 Python 各有生產可用的實作，而包含 Dart 在內的不少語言還沒有），而這個生態逐年變動——導入前查一次該語言的現行實作，不要拿本文的舉例當現況。工具不存在時的替代品是<a href="/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">性質式測試</a>加上人工的斷言強度抽查，不是降級回覆蓋率。</p>
<h2 id="導入順序">導入順序</h2>
<p>先有可信的判準，再談分數。順序顛倒的話，得到的是一套把錯誤需求釘得很牢的高分測試。</p>
<ol>
<li>驗收條件出處獨立、且做過維度交叉盤點</li>
<li>覆蓋率降級成輸入而非閘門（餵給突變測試用來跳過未執行的位置）</li>
<li>突變測試先在核心領域邏輯上跑，不要一開始就全庫</li>
<li>門檻從現況起跳、只要求不倒退，不要一開始就訂絕對值</li>
<li>等價突變的排除清單納入版控，並定期回顧——排除清單長得太快是判斷太寬鬆的訊號</li>
</ol>
<p>這五步自己也是機械約束——<a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278</a> 要求每個機械約束配一個它量不到的維度，而模組入口的「判定問題的答案要留下痕跡」那一節給的是本模組共用的配對形式。這裡只補一件它特有的：門檻採「只要求不倒退」時，最省力的達成是讓新程式落在掃描範圍的 glob 之外，所以<strong>納入率要跟分數並列著看</strong>，兩個數字缺一個就讀不出真相。</p>
<p>這五步預設閘門由團隊自己決定。<strong>門檻是外部義務時——寫進合約的覆蓋率下限、監理或內稽要求的指標——作法是疊加而不是替換</strong>：外部門檻照舊守著，突變分數當內部指標另外跑。有一件事值得寫進驗收流程或合約附件：覆蓋率達標可以用空斷言達成，所以它不構成驗收通過的充分條件；而要讓這句話有效力，它得寫在有約束力的地方，那已經超出這一章能處理的層。同樣的分辨也適用於逐行閱讀——code review 在受監理環境常是變更管理控制項，存在理由是留下具名的責任證據與四眼原則，跟偵測效率無關，那種 review 不能因為「效率低」而移除。</p>
<p>第三點的範圍選擇有實際差別：領域邏輯的突變幾乎都有意義，而 I/O 膠水層、序列化樣板、設定讀取的突變多半是等價的或無關緊要的，先跑那些會讓排除清單暴增而收益很低。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>覆蓋率維持在 90% 以上而故障落在已覆蓋的程式碼裡</td>
          <td>斷言只檢查到有沒有跑完，用突變分數量一次偵測能力</td>
      </tr>
      <tr>
          <td>測試數量大增而信心沒有跟著上升</td>
          <td>量的是規模不是偵測能力，換指標</td>
      </tr>
      <tr>
          <td>突變分數很高而需求層的錯誤還是漏出去</td>
          <td>突變對規格層的錯誤不產生訊號，要補的是出處獨立的驗收條件不是更多突變</td>
      </tr>
      <tr>
          <td>等價突變排除清單快速變長</td>
          <td>判斷太寬鬆，逐條問「留在正式版有沒有輸入看得出差別」</td>
      </tr>
      <tr>
          <td>突變測試跑一次要好幾小時</td>
          <td>改差異模式、用覆蓋率跳過未執行位置、縮短逾時</td>
      </tr>
      <tr>
          <td>新增一條機械閘門而沒指定它量不到什麼</td>
          <td>補上配對檢查點，否則閘門會被最便宜的達成路徑滿足</td>
      </tr>
      <tr>
          <td>「所有指標都綠了」被當成品質改善的結論</td>
          <td>指標描述的是外觀量，要宣稱品質改善需要獨立於該指標的說法</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>判準本身怎麼取得獨立性 → <a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a></li>
<li>驗收條件的射程怎麼量 → <a href="/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類</a></li>
<li>預期值連寫都寫不出來時怎麼辦 → <a href="/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候</a></li>
<li>術語卡 → <a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">Mutation Testing</a></li>
<li>機械約束的代價 → <a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278 機械約束買到被量測的那個數字</a></li>
<li>斷言本身怎麼寫才有鑑別力 → <a href="/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問</a></li>
</ul>
]]></content:encoded></item><item><title>模組六：Agent 產出程式碼的驗證</title><link>https://tarrragon.github.io/blog/testing/06-agent-authored-code/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/06-agent-authored-code/</guid><description>&lt;p>回答「這段程式碼不是我寫的，我憑什麼相信它是對的」。&lt;/p>
&lt;h2 id="這個模組跟前五個模組的分工">這個模組跟前五個模組的分工&lt;/h2>
&lt;p>模組一到五處理的是驗證發生在哪一層、斷言怎麼寫、盲區在哪裡。那些判準本身與作者身分無關，但它們被使用的方式建立在兩個假設上：寫測試的人懂需求，而且有人會讀完程式碼。程式碼由 agent 產出時這兩個假設各失效一半——判準可能與實作出自同一次推導，而讀完整份產出的成本高到多數人不會做。&lt;/p>
&lt;p>失效的不是分層，是&lt;strong>判準的來源與可信度&lt;/strong>。三層測試分層在這裡照舊成立——協議差異仍然要用真實服務驗、畫面狀態仍然要覆蓋；改變的是每一層的預期值從哪裡來，以及那個來源有沒有被實作污染。本模組處理的就是這一層。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>人寫程式時成立的假設&lt;/th>
 &lt;th>Agent 產出時的實情&lt;/th>
 &lt;th>本模組的處理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>測試由懂需求的人寫&lt;/td>
 &lt;td>判準可能從實作推導，編碼的是實作的理解&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗收條件通過即可交付&lt;/td>
 &lt;td>通過的是一個等價類，不是一個程式&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>覆蓋率高代表測試寫得夠&lt;/td>
 &lt;td>覆蓋率可以在斷言為空的情況下達成&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>有人會讀完程式碼&lt;/td>
 &lt;td>逐行讀的成本高到多數人不做，而閱讀原本承擔的那件事沒有自動移交&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源&lt;/a>（人審的位置要跟著移動）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預期值人算得出來&lt;/td>
 &lt;td>產出規模與速度讓逐例斷言跟不上&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="這個模組的證據基礎">這個模組的證據基礎&lt;/h2>
&lt;p>四章共用同一份實證錨：Robert C. Martin 的公開對照實驗 &lt;a href="https://github.com/unclebob/negative-test-experiment">negative-test-experiment&lt;/a>，同一份需求從空目錄寫八次、四種測試紀律乘上一個複雜度度量的開關。案例側的完整記錄在 &lt;a href="https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10&lt;/a>。&lt;/p>
&lt;p>它只有一個產品（一個主控台文字遊戲）、一種語言、每一格執行一次，設計與可讀性評分由原作者本人以主觀量表給出。這決定了它撐得起什麼：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>這份實驗支持的&lt;/th>
 &lt;th>它撐不起的&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同一組驗收條件可以放行行為互不相同的多個程式&lt;/td>
 &lt;td>那個等價類在任一個真實業務系統裡有多大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>行覆蓋率在紀律差異極大的做法之間鑑別力很低&lt;/td>
 &lt;td>覆蓋率門檻在多大的套件規模下才失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>突變測試長出的斷言集中在運算子層&lt;/td>
 &lt;td>突變分數在正式產品上的投資報酬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>施加複雜度上限會增加函式數並降低可讀性&lt;/td>
 &lt;td>那個代價的量級（八列各自獨立重寫，差值含重寫變異）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四章的判準本身出自機制推導，實驗提供的是一次外顯的形態。逐項數字要當成示例讀，不要當成基準值。&lt;/p>
&lt;p>右欄是&lt;strong>量的未知，不是機制的未知&lt;/strong>——「這份實驗撐不起覆蓋率門檻在多大規模才失效」不等於「覆蓋率門檻在我的規模還有效」。拿右欄當有利推定時，要說得出憑什麼。&lt;/p>
&lt;h2 id="判定問題的答案要留下痕跡">判定問題的答案要留下痕跡&lt;/h2>
&lt;p>本模組的判準大多以問句給出——這個交叉消費者觀察得到嗎、這條性質擋不擋得住某個具體的錯誤寫法、這個名字對得回哪個領域概念、這個功能有沒有「兩件事同時發生時該怎麼辦」。&lt;strong>問句本身不產生產物，而認真問過與完全沒問，結論都可以是「不必補」，後者省力得多。&lt;/strong>&lt;/p>
&lt;p>可執行的形式是把答案寫成一列：問了什麼、答案是什麼、依據是哪一條可指認的東西（需求文件的哪一段、可觀察面清單的哪一項、想得到的那個具體錯誤寫法）。&lt;strong>判成不適用、判成暫緩時這一列必填&lt;/strong>——遵循會留下測試碼，豁免什麼都不留，所以舉證責任反過來壓在豁免那一側。&lt;/p>
&lt;p>這是本模組把 &lt;a href="https://tarrragon.github.io/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278&lt;/a> 套用在自己身上的結果：那張卡說機械約束會被最便宜的路徑滿足，而本模組開出的判定問題就是這種約束——最便宜的滿足方式是心裡想一下然後判它不適用。&lt;/p>
&lt;h2 id="資源很少時先做哪一件">資源很少時先做哪一件&lt;/h2>
&lt;p>四章的作法成本差很多。這一節是&lt;strong>授權的降級版本&lt;/strong>，所以進出都要留下痕跡：用它之前寫下哪一項前置不成立（沒有 CI、團隊人數、該語言沒有工具），用它之後訂一個回看的時點或訊號。少了這兩件，「資源很少」會變成不必再檢查的長期狀態。&lt;/p>
&lt;p>前置成立時照這個順序取用：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>判準與實作分開產出&lt;/strong>——開兩個會話、寫測試那一次不餵程式碼。工具層的成本接近零，真正的成本落在需求描述必須自足；擋掉的是最大宗的那一類失效。&lt;/li>
&lt;li>&lt;strong>維度表先攤前四個維度&lt;/strong>（狀態、事件、順序、邊界），這四個靠產品知識就推得出來。併發與失敗需要系統經驗，但&lt;strong>不要整個跳過&lt;/strong>——需求已經明寫的（例如「付款失敗要回滾」）照補，其餘降級成「這個交叉發生時使用者該觀察到什麼」的陳述，實際的競態驗證留給整合層。&lt;/li>
&lt;li>&lt;strong>性質式測試挑兩類&lt;/strong>：往返與冪等。這兩類最好認、也最常在小系統裡成立。&lt;/li>
&lt;li>&lt;strong>突變測試整章暫緩&lt;/strong>。它的兩個硬前置（確定性套件、可用的語言工具）加上一個成本條件（CI 時間預算）通常一個都不成立，強行導入會卡在第零步。暫緩要記進模組的 backlog 並寫明是哪一個前置擋住——回補的訊號跟著那一項走（隔離清單清空、CI 時間預算鬆動、該語言出現生產可用的實作），沒有訊號的暫緩會變成永久豁免。&lt;/li>
&lt;/ol>
&lt;p>反過來說，人手充足而&lt;strong>決定權不在自己手上&lt;/strong>時，順序完全不同——先看&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源&lt;/a>那一章的三層主體表，確認要動的是哪一層。&lt;/p>
&lt;h2 id="本模組回應的案例">本模組回應的案例&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>案例&lt;/th>
 &lt;th>盲區與補位&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10&lt;/a>&lt;/td>
 &lt;td>同一組驗收放行八個不同的程式，判定順序住在沒有被任何條件碰到的維度交叉裡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5&lt;/a>&lt;/td>
 &lt;td>測試自己餵資料造成的同型盲區——同源問題在 stub 層的形態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">LLM 模組四 Benchmarking 與評估&lt;/a>：被測對象本身是模型、輸出不確定時的評估方法住在那裡；本模組處理的是 agent &lt;strong>產出的程式碼&lt;/strong>，兩者的判準形態不同&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/human-ai-collaboration/" data-link-title="4.5 人機協作拓樸：何時人介入、怎麼介入" data-link-desc="Centaur vs Cyborg 工作模式、jagged frontier、HITL 三種觸發時機（pre-act / mid-stream / post-hoc）、確認流程的設計避免橡皮圖章化">LLM 4.5 人機協作拓樸&lt;/a>：那一章決定「人在什麼時機介入、怎麼介入」，本模組的「人審的位置要跟著移動」是同一個決策在驗證側的落點——自動判準的能力決定介入頻率，而判準能力正是本模組四章處理的對象&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend 模組六 可靠性驗證&lt;/a>：Release gate 與 fuzz campaign 是伺服器側的閘門設計，本模組的「品質閘門要成對設計」給的是挑選閘門的判準，兩者互補不重疊&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/record/behavior-first-tdd-methodology/" data-link-title="行為優先的 TDD：測試耦合行為、不耦合結構" data-link-desc="重構時測試大量壞掉、或在「該不該 mock 這個協作者」上需要一個可執行的立場時，本站對兩派 TDD 分歧的選擇與推導">行為優先的 TDD&lt;/a>：站內在古典派與倫敦學派分歧上的立場，本模組承接它留下的下一個問題——行為由誰決定&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277 通過關卡不等於通過的是同一個程式&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278 機械約束買到被量測的那個數字&lt;/a>：本模組兩條主線抽出的可重用原則&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「這段程式碼不是我寫的，我憑什麼相信它是對的」。</p>
<h2 id="這個模組跟前五個模組的分工">這個模組跟前五個模組的分工</h2>
<p>模組一到五處理的是驗證發生在哪一層、斷言怎麼寫、盲區在哪裡。那些判準本身與作者身分無關，但它們被使用的方式建立在兩個假設上：寫測試的人懂需求，而且有人會讀完程式碼。程式碼由 agent 產出時這兩個假設各失效一半——判準可能與實作出自同一次推導，而讀完整份產出的成本高到多數人不會做。</p>
<p>失效的不是分層，是<strong>判準的來源與可信度</strong>。三層測試分層在這裡照舊成立——協議差異仍然要用真實服務驗、畫面狀態仍然要覆蓋；改變的是每一層的預期值從哪裡來，以及那個來源有沒有被實作污染。本模組處理的就是這一層。</p>
<table>
  <thead>
      <tr>
          <th>人寫程式時成立的假設</th>
          <th>Agent 產出時的實情</th>
          <th>本模組的處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>測試由懂需求的人寫</td>
          <td>判準可能從實作推導，編碼的是實作的理解</td>
          <td><a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a></td>
      </tr>
      <tr>
          <td>驗收條件通過即可交付</td>
          <td>通過的是一個等價類，不是一個程式</td>
          <td><a href="/blog/testing/06-agent-authored-code/acceptance-equivalence-class/" data-link-title="驗收條件的等價類：關卡實際固定住了什麼" data-link-desc="一組驗收條件通過就準備交付、卻說不出它放過了哪些行為時，用來攤出條件的維度、找沒被碰過的交叉並決定補哪一條">驗收條件的等價類</a></td>
      </tr>
      <tr>
          <td>覆蓋率高代表測試寫得夠</td>
          <td>覆蓋率可以在斷言為空的情況下達成</td>
          <td><a href="/blog/testing/06-agent-authored-code/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替</a></td>
      </tr>
      <tr>
          <td>有人會讀完程式碼</td>
          <td>逐行讀的成本高到多數人不做，而閱讀原本承擔的那件事沒有自動移交</td>
          <td><a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a>（人審的位置要跟著移動）</td>
      </tr>
      <tr>
          <td>預期值人算得出來</td>
          <td>產出規模與速度讓逐例斷言跟不上</td>
          <td><a href="/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候</a></td>
      </tr>
  </tbody>
</table>
<h2 id="這個模組的證據基礎">這個模組的證據基礎</h2>
<p>四章共用同一份實證錨：Robert C. Martin 的公開對照實驗 <a href="https://github.com/unclebob/negative-test-experiment">negative-test-experiment</a>，同一份需求從空目錄寫八次、四種測試紀律乘上一個複雜度度量的開關。案例側的完整記錄在 <a href="/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10</a>。</p>
<p>它只有一個產品（一個主控台文字遊戲）、一種語言、每一格執行一次，設計與可讀性評分由原作者本人以主觀量表給出。這決定了它撐得起什麼：</p>
<table>
  <thead>
      <tr>
          <th>這份實驗支持的</th>
          <th>它撐不起的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一組驗收條件可以放行行為互不相同的多個程式</td>
          <td>那個等價類在任一個真實業務系統裡有多大</td>
      </tr>
      <tr>
          <td>行覆蓋率在紀律差異極大的做法之間鑑別力很低</td>
          <td>覆蓋率門檻在多大的套件規模下才失效</td>
      </tr>
      <tr>
          <td>突變測試長出的斷言集中在運算子層</td>
          <td>突變分數在正式產品上的投資報酬</td>
      </tr>
      <tr>
          <td>施加複雜度上限會增加函式數並降低可讀性</td>
          <td>那個代價的量級（八列各自獨立重寫，差值含重寫變異）</td>
      </tr>
  </tbody>
</table>
<p>四章的判準本身出自機制推導，實驗提供的是一次外顯的形態。逐項數字要當成示例讀，不要當成基準值。</p>
<p>右欄是<strong>量的未知，不是機制的未知</strong>——「這份實驗撐不起覆蓋率門檻在多大規模才失效」不等於「覆蓋率門檻在我的規模還有效」。拿右欄當有利推定時，要說得出憑什麼。</p>
<h2 id="判定問題的答案要留下痕跡">判定問題的答案要留下痕跡</h2>
<p>本模組的判準大多以問句給出——這個交叉消費者觀察得到嗎、這條性質擋不擋得住某個具體的錯誤寫法、這個名字對得回哪個領域概念、這個功能有沒有「兩件事同時發生時該怎麼辦」。<strong>問句本身不產生產物，而認真問過與完全沒問，結論都可以是「不必補」，後者省力得多。</strong></p>
<p>可執行的形式是把答案寫成一列：問了什麼、答案是什麼、依據是哪一條可指認的東西（需求文件的哪一段、可觀察面清單的哪一項、想得到的那個具體錯誤寫法）。<strong>判成不適用、判成暫緩時這一列必填</strong>——遵循會留下測試碼，豁免什麼都不留，所以舉證責任反過來壓在豁免那一側。</p>
<p>這是本模組把 <a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278</a> 套用在自己身上的結果：那張卡說機械約束會被最便宜的路徑滿足，而本模組開出的判定問題就是這種約束——最便宜的滿足方式是心裡想一下然後判它不適用。</p>
<h2 id="資源很少時先做哪一件">資源很少時先做哪一件</h2>
<p>四章的作法成本差很多。這一節是<strong>授權的降級版本</strong>，所以進出都要留下痕跡：用它之前寫下哪一項前置不成立（沒有 CI、團隊人數、該語言沒有工具），用它之後訂一個回看的時點或訊號。少了這兩件，「資源很少」會變成不必再檢查的長期狀態。</p>
<p>前置成立時照這個順序取用：</p>
<ol>
<li><strong>判準與實作分開產出</strong>——開兩個會話、寫測試那一次不餵程式碼。工具層的成本接近零，真正的成本落在需求描述必須自足；擋掉的是最大宗的那一類失效。</li>
<li><strong>維度表先攤前四個維度</strong>（狀態、事件、順序、邊界），這四個靠產品知識就推得出來。併發與失敗需要系統經驗，但<strong>不要整個跳過</strong>——需求已經明寫的（例如「付款失敗要回滾」）照補，其餘降級成「這個交叉發生時使用者該觀察到什麼」的陳述，實際的競態驗證留給整合層。</li>
<li><strong>性質式測試挑兩類</strong>：往返與冪等。這兩類最好認、也最常在小系統裡成立。</li>
<li><strong>突變測試整章暫緩</strong>。它的兩個硬前置（確定性套件、可用的語言工具）加上一個成本條件（CI 時間預算）通常一個都不成立，強行導入會卡在第零步。暫緩要記進模組的 backlog 並寫明是哪一個前置擋住——回補的訊號跟著那一項走（隔離清單清空、CI 時間預算鬆動、該語言出現生產可用的實作），沒有訊號的暫緩會變成永久豁免。</li>
</ol>
<p>反過來說，人手充足而<strong>決定權不在自己手上</strong>時，順序完全不同——先看<a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a>那一章的三層主體表，確認要動的是哪一層。</p>
<h2 id="本模組回應的案例">本模組回應的案例</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>盲區與補位</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/testing/cases/acceptance-passes-eight-different-programs/" data-link-title="T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大" data-link-desc="同一份需求重寫多次、產出的程式互不相同卻全部通過同一組驗收時，用來看清一組關卡實際固定住了哪些行為，又放過了哪些維度">T.C10</a></td>
          <td>同一組驗收放行八個不同的程式，判定順序住在沒有被任何條件碰到的維度交叉裡</td>
      </tr>
      <tr>
          <td><a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5</a></td>
          <td>測試自己餵資料造成的同型盲區——同源問題在 stub 層的形態</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">LLM 模組四 Benchmarking 與評估</a>：被測對象本身是模型、輸出不確定時的評估方法住在那裡；本模組處理的是 agent <strong>產出的程式碼</strong>，兩者的判準形態不同</li>
<li>→ <a href="/blog/llm/04-applications/human-ai-collaboration/" data-link-title="4.5 人機協作拓樸：何時人介入、怎麼介入" data-link-desc="Centaur vs Cyborg 工作模式、jagged frontier、HITL 三種觸發時機（pre-act / mid-stream / post-hoc）、確認流程的設計避免橡皮圖章化">LLM 4.5 人機協作拓樸</a>：那一章決定「人在什麼時機介入、怎麼介入」，本模組的「人審的位置要跟著移動」是同一個決策在驗證側的落點——自動判準的能力決定介入頻率，而判準能力正是本模組四章處理的對象</li>
<li>→ <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend 模組六 可靠性驗證</a>：Release gate 與 fuzz campaign 是伺服器側的閘門設計，本模組的「品質閘門要成對設計」給的是挑選閘門的判準，兩者互補不重疊</li>
<li>→ <a href="/blog/record/behavior-first-tdd-methodology/" data-link-title="行為優先的 TDD：測試耦合行為、不耦合結構" data-link-desc="重構時測試大量壞掉、或在「該不該 mock 這個協作者」上需要一個可執行的立場時，本站對兩派 TDD 分歧的選擇與推導">行為優先的 TDD</a>：站內在古典派與倫敦學派分歧上的立場，本模組承接它留下的下一個問題——行為由誰決定</li>
<li>→ <a href="/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277 通過關卡不等於通過的是同一個程式</a> 與 <a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278 機械約束買到被量測的那個數字</a>：本模組兩條主線抽出的可重用原則</li>
</ul>
]]></content:encoded></item><item><title>T.C10 同一組驗收通過八個不同的程式 — 關卡放行的等價類有多大</title><link>https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/cases/acceptance-passes-eight-different-programs/</guid><description>&lt;p>&lt;strong>一組關卡通過，證明的是產出落在該關卡切出的等價類裡，不是它就是需求要的那一個。&lt;/strong> 這則案例的價值在於等價類的大小被實際量了出來：同一份需求、同一組驗收，八個互不相同的程式全部放行，其中包含真正的行為分岔。&lt;/p>
&lt;p>本則是&lt;strong>外部案例&lt;/strong>，來源為 Robert C. Martin 的公開實驗 repo &lt;a href="https://github.com/unclebob/negative-test-experiment">negative-test-experiment&lt;/a>。實驗的 policy、plan、八份逐行 notebook 與結論都在 repo 內可回溯；本頁的數字取自 2026 年 8 月的 &lt;code>experiment-conclusion.md&lt;/code>（當時的 HEAD 為 &lt;code>faf9df0&lt;/code>，2026-08-17）。&lt;strong>該 repo 仍在演進&lt;/strong>——作者若補跑新一輪網格或改寫結論，本頁的列號式指認（列 1、列 4、列 5 這類）就會失聯而讀者無從察覺；repo 出現觸及 &lt;code>experiment-conclusion.md&lt;/code> 的新 commit 時，要回頭核對站內所有引用該實驗數字的位置——&lt;code>rg &amp;quot;negative-test-experiment|331 到 603|97% 到 99%|85.81&amp;quot; content/&lt;/code> 是目前掃得到它們的方式。本頁是這些數字的來源，其他位置引用時應指回本頁。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>實驗設計是一個 4 × 2 的網格。產品固定為 Greg Yob 的 Hunt the Wumpus，合規判準固定為一份 25 案例的主控台驗收腳本。變因一是測試紀律四選一：三法則 TDD、test-last（先寫完程式再補測試）、bundling（寫一個函式就測一個函式）、完全不寫測試；變因二是 CRAP 度量是否強制壓到低於 4。八行各自從空目錄寫起，禁止複製其他行的程式碼；每行寫完後封存該行的測試套件，再從空的用突變測試長出第二套。執行者是 AI agent，policy 檔通篇是防止抄近路的條款。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>驗收結果&lt;/td>
 &lt;td>八行全部 25/0 通過，包含零單元測試的那一行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式相異度&lt;/td>
 &lt;td>八棵樹無任何兩棵 checksum 相同；行數 331 到 603、函式數 33 到 118&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>行覆蓋率&lt;/td>
 &lt;td>test-last、bundling、三法則+CRAP 三者都落在 97% 到 99%；三法則不開 CRAP 是 outlier、停在 85.81%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>零測試那一行&lt;/td>
 &lt;td>覆蓋率 14%（form）/ 24%（line），數字全部來自載入而非斷言，驗收仍然全過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最密的測試套件&lt;/td>
 &lt;td>206 個範例、277 個斷言，而驗收結果與零測試那一行完全一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>突變測試的產出&lt;/td>
 &lt;td>補上運算子層的翻轉（&lt;code>if&lt;/code>/&lt;code>if-not&lt;/code>、&lt;code>=&lt;/code>/&lt;code>not=&lt;/code>、&lt;code>1&lt;/code>/&lt;code>0&lt;/code>），未發現不同的遊戲&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>被放行的差異裡有三類不只是實作自由度：&lt;/p>
&lt;p>&lt;strong>危險判定順序。&lt;/strong> 原作的規則是先檢查 wumpus、再 pit、再 bats。八行裡有四行照做（列 3、6、7、8）；另外三行（列 1、4、5）的 notebook 逐行記載都是 bats、pit、wumpus；列 2 的 notebook 未記順序。實際存在的是&lt;strong>兩種&lt;/strong>順序，而驗收案例從來沒有把兩個危險放進同一個房間，所以兩種都合法通過。&lt;/p>
&lt;p>&lt;strong>終止行為。&lt;/strong> 一行的主迴圈永不結束，一行的外層迴圈永不離開，一行以拋出並吞掉例外的方式結束。驗收腳本沒有斷言離開的方式。&lt;/p>
&lt;p>&lt;strong>亂數來源。&lt;/strong> 三種不同的產生器（平台內建、兩種不同係數的線性同餘），意味著 &lt;code>--seed&lt;/code> 參數在八行之間不是可攜的契約——同一個種子在不同行產出不同的地圖。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>等價類的大小由條件之間的交叉密度決定，不由條件數量決定。&lt;/strong> 二十五條驗收案例讀起來像把功能講完了，實際上它們只約束了「每個危險單獨出現時的反應」。判定順序住在「兩個危險同時出現」這個交叉裡，而沒有任何一條條件走進那裡。這與&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/three-layer-definition/" data-link-title="三層定義與職責表" data-link-desc="Unit Test / Protocol Integration Test / Screen State Test 各層職責、驗證目標與盲區的完整論述">三層測試分層&lt;/a>談的盲區不同——那是層與層之間的縫，這是同一層內部維度沒有相乘。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>同一維度上加斷言，縮小等價類的幅度隨密度遞減；對沒有任何條件碰過的維度，增量是零。&lt;/strong> 八行的單元測試規模從零到 206 個範例，而驗收結果完全一致——判定順序住在「兩個危險同時出現」這個交叉裡，25 條驗收條件一條都沒有走進去。密度成長與涵蓋面成長是兩件事，而套件規模只反映前者。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>行覆蓋率在這個網格裡鑑別力很低。&lt;/strong> test-last、bundling、三法則+CRAP 三者都停在 97% 到 99%，而它們的程式與套件組成完全不同。被覆蓋率區分出來的只有兩端：零測試那一行的 14% / 24%，以及三法則不開 CRAP 的 85.81%（作者標為 outlier，成因是主控台迴圈只被輕度驅動）。也就是說覆蓋率分得出「有沒有測試」與「有沒有一整塊沒被驅動」，分不出「測試好不好」。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>突變測試守得住斷言密度，守不住規格錯誤。&lt;/strong> 實驗記載的是突變長出來的第二套測試全部瞄準運算子層的翻轉、作者的結論是它「沒有發現一個不同的遊戲」。由此可以推出而實驗未直接測量的一件事：突變是對照著程式&lt;strong>現在的樣子&lt;/strong>產生的，所以它不會對判定順序產生任何訊號——把 bats 排在前面的那份程式跑突變測試，長出來的斷言會把那個順序釘得更牢。這個限制在測試與實作&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">出處不獨立&lt;/a>時特別要緊，因為那正是規格層錯誤最容易產生的情境。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>自由度與未規定的需求要分開。&lt;/strong> 等價類大本身不是缺陷——命名、模組切法、內部資料結構本來就該留給實作。八行之間的檔案切法差異屬於這一類；判定順序與終止行為不屬於，它們是需求的一部分，只是沒有被寫進條件裡。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>從這則案例抽出的可重用修法在 &lt;a href="https://tarrragon.github.io/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277&lt;/a>（設計驗收條件時問哪些不同的程式會通過、交叉優先於單維度密度、把通過的產出當成樣本、指名誰接手了閱讀原本承擔的工作、區分自由度與未規定的需求），這裡不重述。&lt;/p>
&lt;p>案例層特有的只有一條：&lt;strong>把品質指標配對到它量不到的維度&lt;/strong>。覆蓋率門檻配&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">突變測試&lt;/a>，突變分數再配一次規格層的人工檢查——本則的八個版本正好示範了為什麼單一指標全綠不構成放寬下一層檢查的理由。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>這則案例抽出的可重用原則 → &lt;a href="https://tarrragon.github.io/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277 通過關卡不等於通過的是同一個程式&lt;/a>&lt;/li>
&lt;li>同一份實驗的另一軸（機械約束的代價）→ &lt;a href="https://tarrragon.github.io/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278 機械約束買到被量測的那個數字&lt;/a>&lt;/li>
&lt;li>測試與實作同源時 oracle 為什麼退化 → &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">Test Provenance&lt;/a>&lt;/li>
&lt;li>判準來源本身有哪幾種、各自抓得到什麼 → &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">Test Oracle&lt;/a>&lt;/li>
&lt;li>由測試自己餵資料造成同型盲區的自有案例 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p><strong>一組關卡通過，證明的是產出落在該關卡切出的等價類裡，不是它就是需求要的那一個。</strong> 這則案例的價值在於等價類的大小被實際量了出來：同一份需求、同一組驗收，八個互不相同的程式全部放行，其中包含真正的行為分岔。</p>
<p>本則是<strong>外部案例</strong>，來源為 Robert C. Martin 的公開實驗 repo <a href="https://github.com/unclebob/negative-test-experiment">negative-test-experiment</a>。實驗的 policy、plan、八份逐行 notebook 與結論都在 repo 內可回溯；本頁的數字取自 2026 年 8 月的 <code>experiment-conclusion.md</code>（當時的 HEAD 為 <code>faf9df0</code>，2026-08-17）。<strong>該 repo 仍在演進</strong>——作者若補跑新一輪網格或改寫結論，本頁的列號式指認（列 1、列 4、列 5 這類）就會失聯而讀者無從察覺；repo 出現觸及 <code>experiment-conclusion.md</code> 的新 commit 時，要回頭核對站內所有引用該實驗數字的位置——<code>rg &quot;negative-test-experiment|331 到 603|97% 到 99%|85.81&quot; content/</code> 是目前掃得到它們的方式。本頁是這些數字的來源，其他位置引用時應指回本頁。</p>
<h2 id="觀察">觀察</h2>
<p>實驗設計是一個 4 × 2 的網格。產品固定為 Greg Yob 的 Hunt the Wumpus，合規判準固定為一份 25 案例的主控台驗收腳本。變因一是測試紀律四選一：三法則 TDD、test-last（先寫完程式再補測試）、bundling（寫一個函式就測一個函式）、完全不寫測試；變因二是 CRAP 度量是否強制壓到低於 4。八行各自從空目錄寫起，禁止複製其他行的程式碼；每行寫完後封存該行的測試套件，再從空的用突變測試長出第二套。執行者是 AI agent，policy 檔通篇是防止抄近路的條款。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗收結果</td>
          <td>八行全部 25/0 通過，包含零單元測試的那一行</td>
      </tr>
      <tr>
          <td>程式相異度</td>
          <td>八棵樹無任何兩棵 checksum 相同；行數 331 到 603、函式數 33 到 118</td>
      </tr>
      <tr>
          <td>行覆蓋率</td>
          <td>test-last、bundling、三法則+CRAP 三者都落在 97% 到 99%；三法則不開 CRAP 是 outlier、停在 85.81%</td>
      </tr>
      <tr>
          <td>零測試那一行</td>
          <td>覆蓋率 14%（form）/ 24%（line），數字全部來自載入而非斷言，驗收仍然全過</td>
      </tr>
      <tr>
          <td>最密的測試套件</td>
          <td>206 個範例、277 個斷言，而驗收結果與零測試那一行完全一致</td>
      </tr>
      <tr>
          <td>突變測試的產出</td>
          <td>補上運算子層的翻轉（<code>if</code>/<code>if-not</code>、<code>=</code>/<code>not=</code>、<code>1</code>/<code>0</code>），未發現不同的遊戲</td>
      </tr>
  </tbody>
</table>
<p>被放行的差異裡有三類不只是實作自由度：</p>
<p><strong>危險判定順序。</strong> 原作的規則是先檢查 wumpus、再 pit、再 bats。八行裡有四行照做（列 3、6、7、8）；另外三行（列 1、4、5）的 notebook 逐行記載都是 bats、pit、wumpus；列 2 的 notebook 未記順序。實際存在的是<strong>兩種</strong>順序，而驗收案例從來沒有把兩個危險放進同一個房間，所以兩種都合法通過。</p>
<p><strong>終止行為。</strong> 一行的主迴圈永不結束，一行的外層迴圈永不離開，一行以拋出並吞掉例外的方式結束。驗收腳本沒有斷言離開的方式。</p>
<p><strong>亂數來源。</strong> 三種不同的產生器（平台內建、兩種不同係數的線性同餘），意味著 <code>--seed</code> 參數在八行之間不是可攜的契約——同一個種子在不同行產出不同的地圖。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>等價類的大小由條件之間的交叉密度決定，不由條件數量決定。</strong> 二十五條驗收案例讀起來像把功能講完了，實際上它們只約束了「每個危險單獨出現時的反應」。判定順序住在「兩個危險同時出現」這個交叉裡，而沒有任何一條條件走進那裡。這與<a href="/blog/testing/01-test-strategy-layers/three-layer-definition/" data-link-title="三層定義與職責表" data-link-desc="Unit Test / Protocol Integration Test / Screen State Test 各層職責、驗證目標與盲區的完整論述">三層測試分層</a>談的盲區不同——那是層與層之間的縫，這是同一層內部維度沒有相乘。</p>
</li>
<li>
<p><strong>同一維度上加斷言，縮小等價類的幅度隨密度遞減；對沒有任何條件碰過的維度，增量是零。</strong> 八行的單元測試規模從零到 206 個範例，而驗收結果完全一致——判定順序住在「兩個危險同時出現」這個交叉裡，25 條驗收條件一條都沒有走進去。密度成長與涵蓋面成長是兩件事，而套件規模只反映前者。</p>
</li>
<li>
<p><strong>行覆蓋率在這個網格裡鑑別力很低。</strong> test-last、bundling、三法則+CRAP 三者都停在 97% 到 99%，而它們的程式與套件組成完全不同。被覆蓋率區分出來的只有兩端：零測試那一行的 14% / 24%，以及三法則不開 CRAP 的 85.81%（作者標為 outlier，成因是主控台迴圈只被輕度驅動）。也就是說覆蓋率分得出「有沒有測試」與「有沒有一整塊沒被驅動」，分不出「測試好不好」。</p>
</li>
<li>
<p><strong>突變測試守得住斷言密度，守不住規格錯誤。</strong> 實驗記載的是突變長出來的第二套測試全部瞄準運算子層的翻轉、作者的結論是它「沒有發現一個不同的遊戲」。由此可以推出而實驗未直接測量的一件事：突變是對照著程式<strong>現在的樣子</strong>產生的，所以它不會對判定順序產生任何訊號——把 bats 排在前面的那份程式跑突變測試，長出來的斷言會把那個順序釘得更牢。這個限制在測試與實作<a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">出處不獨立</a>時特別要緊，因為那正是規格層錯誤最容易產生的情境。</p>
</li>
<li>
<p><strong>自由度與未規定的需求要分開。</strong> 等價類大本身不是缺陷——命名、模組切法、內部資料結構本來就該留給實作。八行之間的檔案切法差異屬於這一類；判定順序與終止行為不屬於，它們是需求的一部分，只是沒有被寫進條件裡。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<p>從這則案例抽出的可重用修法在 <a href="/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277</a>（設計驗收條件時問哪些不同的程式會通過、交叉優先於單維度密度、把通過的產出當成樣本、指名誰接手了閱讀原本承擔的工作、區分自由度與未規定的需求），這裡不重述。</p>
<p>案例層特有的只有一條：<strong>把品質指標配對到它量不到的維度</strong>。覆蓋率門檻配<a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">突變測試</a>，突變分數再配一次規格層的人工檢查——本則的八個版本正好示範了為什麼單一指標全綠不構成放寬下一層檢查的理由。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>這則案例抽出的可重用原則 → <a href="/blog/report/passing-a-gate-does-not-pin-the-program/" data-link-title="通過關卡不等於通過的是同一個程式" data-link-desc="把驗證交給一組自動化關卡、並據此不再逐行讀產出時，用來判斷這組關卡實際固定住了什麼、放過了哪些維度">#277 通過關卡不等於通過的是同一個程式</a></li>
<li>同一份實驗的另一軸（機械約束的代價）→ <a href="/blog/report/mechanical-constraints-buy-the-measured-number/" data-link-title="機械約束買到被量測的那個數字，代價落在沒被量測的維度" data-link-desc="用函式長度、圈複雜度、覆蓋率門檻這類可自動檢查的品質約束時，用來判斷達成它換到了什麼、又付出了什麼">#278 機械約束買到被量測的那個數字</a></li>
<li>測試與實作同源時 oracle 為什麼退化 → <a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">Test Provenance</a></li>
<li>判準來源本身有哪幾種、各自抓得到什麼 → <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">Test Oracle</a></li>
<li>由測試自己餵資料造成同型盲區的自有案例 → <a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽</a></li>
</ul>
]]></content:encoded></item><item><title>Test Provenance（測試出處）</title><link>https://tarrragon.github.io/blog/testing/knowledge-cards/test-provenance/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/knowledge-cards/test-provenance/</guid><description>&lt;p>測試出處指的是測試與被測實作各自由誰、在什麼順序下產出。它是一項特定驗證力的&lt;strong>代理指標&lt;/strong>——抓出需求被誤讀的能力，真正取決於&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準是不是由實作推導&lt;/a>出來的，而出處是這件事最好查的線索：同一次產出的兩者幾乎一定共用同一份對需求的理解，於是斷言編碼的是實作以為自己該做什麼，&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle&lt;/a> 退化成實作自身的回音。&lt;/p>
&lt;p>代理會在兩個位置失準，記住它們就知道什麼時候不能只看出處：同源產出而判準是一條從需求來的不變量時，驗證力仍在；獨立產出而寫測試那一方被餵了 API schema 時，判準仍有一部分是推導來的。這跟 &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/stub/" data-link-title="Stub" data-link-desc="測試作者手動寫死回應資料的 test double：驗證的是假設成立時邏輯是否正確，假設本身錯誤時無法檢出">stub&lt;/a> 那張卡描述的失效是同一個機制在不同層級的形態——stub 是餵資料的人同時寫斷言，出處問題是寫實作的人同時寫斷言。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>出處是 &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle 來源&lt;/a>之外的第二個維度。同一個 oracle 類型可以有不同的出處：規格 oracle 由人寫規格、機器寫實作時獨立，由機器同時產出規格與實作時不獨立。測試三層（unit / &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">protocol integration&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">screen state&lt;/a>）對這個維度不作區分——分層決定驗證發生在哪，出處決定驗證是不是在照鏡子。&lt;/p>
&lt;p>判準被污染的程度是連續的，不是有無：判準完全從實作推導時抓不到任何需求誤讀，判準有一部分從實作推導（例如生成器的參數、性質的前置條件抄自實作的守衛條件）時抓得到一部分。形態的分類、各自的射程、以及可動的四個變數在&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源&lt;/a>那一章，卡片這裡不重複一套鍵不同的分類。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>最直接的訊號是順序：測試在實作之後由同一個來源補上時，它多半是照著實作的分支寫出來的，覆蓋率數字會很高而斷言貼著實作結構。另一個訊號是斷言的寫法——斷言重述實作的計算式（實作寫 &lt;code>a * 0.8&lt;/code>、斷言也算 &lt;code>a * 0.8&lt;/code>）：預期值是從實作推導來的，需求文件裡的那個數字從頭到尾沒被引用。&lt;/p>
&lt;p>規格層的錯誤是這類測試的系統性盲區。危險處理順序寫反、狀態機少一個轉換、幣別換算方向顛倒——這些在實作與測試共用同一個誤解時全部通過，因為兩邊錯得一致。&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">mutation testing&lt;/a> 也發現不了它們，理由與突變的產生方式有關、在那張卡上。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>出處是可以被設計的，不是給定的：需求的來源、產出的順序、產出的主體、以及兩者之間的時間間隔，四者各自可動。四個變數的取捨、成本與操作方式在&lt;a href="https://tarrragon.github.io/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源&lt;/a>那一章展開。&lt;/p>
&lt;p>卡片層要記住的判準只有一條：四者都動不了時，這組測試的定位要降級成回歸網，並且在別處補上獨立的判準。&lt;/p></description><content:encoded><![CDATA[<p>測試出處指的是測試與被測實作各自由誰、在什麼順序下產出。它是一項特定驗證力的<strong>代理指標</strong>——抓出需求被誤讀的能力，真正取決於<a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準是不是由實作推導</a>出來的，而出處是這件事最好查的線索：同一次產出的兩者幾乎一定共用同一份對需求的理解，於是斷言編碼的是實作以為自己該做什麼，<a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle</a> 退化成實作自身的回音。</p>
<p>代理會在兩個位置失準，記住它們就知道什麼時候不能只看出處：同源產出而判準是一條從需求來的不變量時，驗證力仍在；獨立產出而寫測試那一方被餵了 API schema 時，判準仍有一部分是推導來的。這跟 <a href="/blog/testing/knowledge-cards/stub/" data-link-title="Stub" data-link-desc="測試作者手動寫死回應資料的 test double：驗證的是假設成立時邏輯是否正確，假設本身錯誤時無法檢出">stub</a> 那張卡描述的失效是同一個機制在不同層級的形態——stub 是餵資料的人同時寫斷言，出處問題是寫實作的人同時寫斷言。</p>
<h2 id="概念位置">概念位置</h2>
<p>出處是 <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle 來源</a>之外的第二個維度。同一個 oracle 類型可以有不同的出處：規格 oracle 由人寫規格、機器寫實作時獨立，由機器同時產出規格與實作時不獨立。測試三層（unit / <a href="/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">protocol integration</a> / <a href="/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">screen state</a>）對這個維度不作區分——分層決定驗證發生在哪，出處決定驗證是不是在照鏡子。</p>
<p>判準被污染的程度是連續的，不是有無：判準完全從實作推導時抓不到任何需求誤讀，判準有一部分從實作推導（例如生成器的參數、性質的前置條件抄自實作的守衛條件）時抓得到一部分。形態的分類、各自的射程、以及可動的四個變數在<a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a>那一章，卡片這裡不重複一套鍵不同的分類。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>最直接的訊號是順序：測試在實作之後由同一個來源補上時，它多半是照著實作的分支寫出來的，覆蓋率數字會很高而斷言貼著實作結構。另一個訊號是斷言的寫法——斷言重述實作的計算式（實作寫 <code>a * 0.8</code>、斷言也算 <code>a * 0.8</code>）：預期值是從實作推導來的，需求文件裡的那個數字從頭到尾沒被引用。</p>
<p>規格層的錯誤是這類測試的系統性盲區。危險處理順序寫反、狀態機少一個轉換、幣別換算方向顛倒——這些在實作與測試共用同一個誤解時全部通過，因為兩邊錯得一致。<a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">mutation testing</a> 也發現不了它們，理由與突變的產生方式有關、在那張卡上。</p>
<h2 id="設計責任">設計責任</h2>
<p>出處是可以被設計的，不是給定的：需求的來源、產出的順序、產出的主體、以及兩者之間的時間間隔，四者各自可動。四個變數的取捨、成本與操作方式在<a href="/blog/testing/06-agent-authored-code/test-provenance-independence/" data-link-title="判準的推導來源：測試憑什麼不是實作的回音" data-link-desc="測試與實作由同一次生成或同一輪對話產出時，用來判斷這組測試剩下多少驗證力，以及要動哪個變數才能拿回來">判準的推導來源</a>那一章展開。</p>
<p>卡片層要記住的判準只有一條：四者都動不了時，這組測試的定位要降級成回歸網，並且在別處補上獨立的判準。</p>
]]></content:encoded></item></channel></rss>