<?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>模組六：Agent 產出程式碼的驗證 on Tarragon</title><link>https://tarrragon.github.io/blog/testing/06-agent-authored-code/</link><description>Recent content in 模組六：Agent 產出程式碼的驗證 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/testing/06-agent-authored-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>判準寫不下來的時候：性質、變形關係與留給人的部分</title><link>https://tarrragon.github.io/blog/testing/06-agent-authored-code/oracle-beyond-examples/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/06-agent-authored-code/oracle-beyond-examples/</guid><description>&lt;p>判準的形態可以退階：算得出每個輸入的正確答案時用逐例斷言，算不出答案但寫得出答案必須滿足的條件時用&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;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/metamorphic-testing/" data-link-title="Metamorphic Testing（變形測試）" data-link-desc="被測對象沒有已知正確輸出、連不變量都難以陳述時，用兩次執行之間的關係取代預期值">變形關係&lt;/a>。三者的共同點是判準仍然由人決定；再往下就是判準本身無法事先寫下的區域，那裡的工作交不出去。&lt;/p>
&lt;p>這條退階在 agent 產出的情境下比過去更常用到，原因有兩個：產出的規模讓逐例斷言的撰寫速度成為瓶頸，而逐例的預期值又是最容易被實作污染的一種——跑一次實作就能把答案抄進斷言。不變量與變形關係&lt;strong>比較不容易被抄&lt;/strong>：它們沒有現成的形態可以複製，要從實作反推得先看出一條性質，那是一個刻意的動作而非順手的動作。這是機率的差別不是構造上的不可能——從實作觀察出一條對應再寫成斷言仍然辦得到，而那正是本章末段要處理的失效模式。&lt;/p>
&lt;h2 id="三層退階的選用">三層退階的選用&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>判準形態&lt;/th>
 &lt;th>需要事先知道&lt;/th>
 &lt;th>適用時機&lt;/th>
 &lt;th>對&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">出處污染&lt;/a>的抵抗力&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;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>變形關係&lt;/td>
 &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;strong>錯了的代價&lt;/strong>決定下界（代價高、或這段邏輯會被重試與併發碰到，就一路補到變形關係）。選用不是三選一，同一個功能常常三層都有：折扣計算的幾個關鍵金額用逐例斷言釘死，「折扣後金額不超過原價且不小於零」是不變量，「數量加倍時折扣後總額不會少於原本」是變形關係。三者各自抓不同的錯，而後兩者在需求被誤讀時仍然會變紅。&lt;/p>
&lt;h2 id="不變量怎麼寫得有鑑別力">不變量怎麼寫得有鑑別力&lt;/h2>
&lt;p>性質太弱是這類測試主要的失效方式。「結果不會是 null」對所有實作都成立，包含錯的那些。判斷方式與突變測試一致——&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;strong>對照。&lt;/strong> 與一份簡單但慢的參照實作結果相同。最佳化過的演算法是最直接的用法，而業務系統裡更常見的是另外兩種：費率或稅務試算拿還在跑的舊系統當參照（遷移期間免費附贈一份參照實作），報表聚合拿一句直白的 SQL 對照程式裡那套分批累加的邏輯。划不划算看一件事——&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>生成器要跟著資料的真實形狀走，而「真實形狀」要靠量測取得。可執行的取法是從正式環境的資料抽一份樣本（拿不到正式資料時——新系統、或沒有存取權——退而求其次去量既有的錯誤日誌與客訴紀錄，那裡的輸入形狀最接近真實的極端），對每個欄位量四件事——長度分佈的極值與中位數、字元集（有沒有多位元組、控制字元、前後空白）、空值與缺欄的實際比例、以及重複值的集中度——再把這四項餵回生成器的參數。預設生成器產出的字串多半是短的隨機 ASCII，量過就會知道它離真實有多遠。生成器沒有涵蓋的形狀，性質再強也驗不到，這與&lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">Test data 代表性&lt;/a>是同一個問題。&lt;/p>
&lt;p>失敗時框架回報的最短反例是產出本身——它通常直接就是缺陷的最小重現案例，可以原樣固定成一個逐例測試，讓這次的具體失敗有穩定的回歸點。&lt;/p>
&lt;h2 id="變形關係適用的位置">變形關係適用的位置&lt;/h2>
&lt;p>適用訊號是「跑出來的結果對不對沒有人說得準，但如果它變成這樣就一定錯了」。推薦排序、搜尋相關度、路徑規劃、費率與稅務試算、以及任何輸出帶有不確定性的元件，都落在這一類。&lt;/p>
&lt;p>關係的方向要從業務語意選，不同的關係抓不同的缺陷：&lt;/p>
&lt;ul>
&lt;li>搜尋條件收緊時結果集應該是原本的子集——抓過濾邏輯&lt;/li>
&lt;li>輸入清單順序打亂時結果集應該完全相同——抓對輸入順序的隱性依賴&lt;/li>
&lt;li>所有價格同乘一個係數時總價同比例變化——抓計算裡漏掉的加項&lt;/li>
&lt;/ul>
&lt;p>關係要從需求推導而不是從實作觀察。跑兩次看到某個對應成立就把它寫成斷言，等於把實作現在的行為當成規格，那是出處污染的另一種形態。&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;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">明確的分界&lt;/a>：判準在執行之前寫得下來的屬於檢查，可以交給機器重複執行；判準要在觀察當下才成形的屬於探究，交不出去。同一個功能通常兩側都有——登入失敗三次鎖定帳號是檢查，鎖定之後的求助路徑通不通要有人真的走一次。&lt;/p>
&lt;p>資源分配要跟著改的第一件事，是不再拿套件規模當思考量的代理指標——兩者為什麼脫鉤，&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">testing vs checking&lt;/a> 那張卡有完整的推導。&lt;/p>
&lt;p>實際的分配方式是把省下來的人力移到檢查產生不了的地方：判準本身的設計、驗收條件的維度盤點、以及那些沒有被任何斷言涵蓋的問題。把省下來的時間拿去逐行讀機器寫的斷言，等於把人放回機器已經做得比較好的那一側。&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>「結果不會是 null」這種性質&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;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;/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/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-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">Test Oracle&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">Property-Based Testing&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/metamorphic-testing/" data-link-title="Metamorphic Testing（變形測試）" data-link-desc="被測對象沒有已知正確輸出、連不變量都難以陳述時，用兩次執行之間的關係取代預期值">Metamorphic Testing&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">Testing vs Checking&lt;/a>&lt;/li>
&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>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>判準的形態可以退階：算得出每個輸入的正確答案時用逐例斷言，算不出答案但寫得出答案必須滿足的條件時用<a href="/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">不變量</a>，連條件都寫不出來但知道輸入變化與輸出變化的對應時用<a href="/blog/testing/knowledge-cards/metamorphic-testing/" data-link-title="Metamorphic Testing（變形測試）" data-link-desc="被測對象沒有已知正確輸出、連不變量都難以陳述時，用兩次執行之間的關係取代預期值">變形關係</a>。三者的共同點是判準仍然由人決定；再往下就是判準本身無法事先寫下的區域，那裡的工作交不出去。</p>
<p>這條退階在 agent 產出的情境下比過去更常用到，原因有兩個：產出的規模讓逐例斷言的撰寫速度成為瓶頸，而逐例的預期值又是最容易被實作污染的一種——跑一次實作就能把答案抄進斷言。不變量與變形關係<strong>比較不容易被抄</strong>：它們沒有現成的形態可以複製，要從實作反推得先看出一條性質，那是一個刻意的動作而非順手的動作。這是機率的差別不是構造上的不可能——從實作觀察出一條對應再寫成斷言仍然辦得到，而那正是本章末段要處理的失效模式。</p>
<h2 id="三層退階的選用">三層退階的選用</h2>
<table>
  <thead>
      <tr>
          <th>判準形態</th>
          <th>需要事先知道</th>
          <th>適用時機</th>
          <th>對<a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">出處污染</a>的抵抗力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>逐例斷言</td>
          <td>每個輸入對應的正確輸出</td>
          <td>規格明確、答案算得出來</td>
          <td>低——可從實作抄回</td>
      </tr>
      <tr>
          <td>不變量</td>
          <td>所有輸出都該滿足的條件</td>
          <td>答案算不出來、條件寫得出來</td>
          <td>高——必須從需求推導</td>
      </tr>
      <tr>
          <td>變形關係</td>
          <td>輸入變化與輸出變化的對應</td>
          <td>條件也寫不出來、只知相對關係</td>
          <td>高——同樣要從需求推導、實作裡沒有現成形態可抄</td>
      </tr>
  </tbody>
</table>
<p>選幾層看兩件事——<strong>答案算不算得出來</strong>決定上界（算得出來就從逐例開始），<strong>錯了的代價</strong>決定下界（代價高、或這段邏輯會被重試與併發碰到，就一路補到變形關係）。選用不是三選一，同一個功能常常三層都有：折扣計算的幾個關鍵金額用逐例斷言釘死，「折扣後金額不超過原價且不小於零」是不變量，「數量加倍時折扣後總額不會少於原本」是變形關係。三者各自抓不同的錯，而後兩者在需求被誤讀時仍然會變紅。</p>
<h2 id="不變量怎麼寫得有鑑別力">不變量怎麼寫得有鑑別力</h2>
<p>性質太弱是這類測試主要的失效方式。「結果不會是 null」對所有實作都成立，包含錯的那些。判斷方式與突變測試一致——<strong>問這條性質擋不擋得住某個具體的錯誤寫法</strong>。「想出一個具體的錯誤寫法」這一步交不出去，可執行的只有「想不出來就代表性質太弱」那半句。</p>
<p>下面六類在業務系統裡最常成立，前四類是骨幹、後兩類補上骨幹漏掉的形態。<strong>用法是逐類問「這個功能上它成不成立」、不成立寫一句理由</strong>——挑兩三類寫完就收工是最省力的走法，而模組的核心論證（涵蓋面不等於密度）在這裡同樣適用：六類裡沒被問過的那幾類，增量是零。</p>
<p><strong>往返。</strong> 編碼後解碼得回原值。適用於序列化、加解密、格式轉換、匯出匯入。這一類的鑑別力來自它同時約束兩個方向，單邊寫錯就會被抓到。</p>
<p><strong>守恆。</strong> 操作前後某個量不變或單調變化。轉帳前後總額相同、合併訂單後品項總數不變、分頁遍歷完的元素數等於總筆數。這一類特別適合抓「資料在某條路徑上被吃掉」的缺陷，而那類缺陷逐例斷言很難覆蓋到。</p>
<p><strong>對照。</strong> 與一份簡單但慢的參照實作結果相同。最佳化過的演算法是最直接的用法，而業務系統裡更常見的是另外兩種：費率或稅務試算拿還在跑的舊系統當參照（遷移期間免費附贈一份參照實作），報表聚合拿一句直白的 SQL 對照程式裡那套分批累加的邏輯。划不划算看一件事——<strong>寫那份參照實作要花多久</strong>：邏輯本身十行內寫得完就值得，要重建一套領域模型才寫得出來就不值得，那時改用守恆性質。</p>
<p><strong>冪等。</strong> 執行兩次與執行一次結果相同。適用於正規化、去重、部署腳本、以及所有會被重試的操作。這一類在有重試機制的系統裡是必須的，而它幾乎不會出現在逐例測試裡。</p>
<p><strong>上下界。</strong> 輸出永遠落在某個範圍內——折扣後的金額不超過原價也不小於零、庫存不會是負數、百分比不超過一百。最容易寫、鑑別力也最低，但它抓的那一類缺陷（溢位、正負號、單位換算）逐例測試常常整批漏掉。</p>
<p><strong>順序無關。</strong> 打亂輸入的順序，結果應該相同。分散式與批次處理裡最要緊的一類——訊息亂序抵達、分區處理順序不固定、平行聚合的合併次序，這些在單執行緒的逐例測試裡永遠不會出現。</p>
<p>生成器要跟著資料的真實形狀走，而「真實形狀」要靠量測取得。可執行的取法是從正式環境的資料抽一份樣本（拿不到正式資料時——新系統、或沒有存取權——退而求其次去量既有的錯誤日誌與客訴紀錄，那裡的輸入形狀最接近真實的極端），對每個欄位量四件事——長度分佈的極值與中位數、字元集（有沒有多位元組、控制字元、前後空白）、空值與缺欄的實際比例、以及重複值的集中度——再把這四項餵回生成器的參數。預設生成器產出的字串多半是短的隨機 ASCII，量過就會知道它離真實有多遠。生成器沒有涵蓋的形狀，性質再強也驗不到，這與<a href="/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">Test data 代表性</a>是同一個問題。</p>
<p>失敗時框架回報的最短反例是產出本身——它通常直接就是缺陷的最小重現案例，可以原樣固定成一個逐例測試，讓這次的具體失敗有穩定的回歸點。</p>
<h2 id="變形關係適用的位置">變形關係適用的位置</h2>
<p>適用訊號是「跑出來的結果對不對沒有人說得準，但如果它變成這樣就一定錯了」。推薦排序、搜尋相關度、路徑規劃、費率與稅務試算、以及任何輸出帶有不確定性的元件，都落在這一類。</p>
<p>關係的方向要從業務語意選，不同的關係抓不同的缺陷：</p>
<ul>
<li>搜尋條件收緊時結果集應該是原本的子集——抓過濾邏輯</li>
<li>輸入清單順序打亂時結果集應該完全相同——抓對輸入順序的隱性依賴</li>
<li>所有價格同乘一個係數時總價同比例變化——抓計算裡漏掉的加項</li>
</ul>
<p>關係要從需求推導而不是從實作觀察。跑兩次看到某個對應成立就把它寫成斷言，等於把實作現在的行為當成規格，那是出處污染的另一種形態。</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>這一區與可自動化的部分之間有一條<a href="/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">明確的分界</a>：判準在執行之前寫得下來的屬於檢查，可以交給機器重複執行；判準要在觀察當下才成形的屬於探究，交不出去。同一個功能通常兩側都有——登入失敗三次鎖定帳號是檢查，鎖定之後的求助路徑通不通要有人真的走一次。</p>
<p>資源分配要跟著改的第一件事，是不再拿套件規模當思考量的代理指標——兩者為什麼脫鉤，<a href="/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">testing vs checking</a> 那張卡有完整的推導。</p>
<p>實際的分配方式是把省下來的人力移到檢查產生不了的地方：判準本身的設計、驗收條件的維度盤點、以及那些沒有被任何斷言涵蓋的問題。把省下來的時間拿去逐行讀機器寫的斷言，等於把人放回機器已經做得比較好的那一側。</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>「結果不會是 null」這種性質</td>
          <td>鑑別力不足，問它擋不擋得住某個具體的錯誤寫法</td>
      </tr>
      <tr>
          <td>性質全綠而真實輸入形狀與生成器差很遠</td>
          <td>生成器沒涵蓋的形狀驗不到，先修生成器</td>
      </tr>
      <tr>
          <td>有重試機制而沒有任何冪等性斷言</td>
          <td>補冪等性質，這類缺陷逐例測試幾乎抓不到</td>
      </tr>
      <tr>
          <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>
      </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/coverage-to-mutation-gate/" data-link-title="品質閘門的更替：覆蓋率、突變分數與各自的射程" data-link-desc="測試數量大幅增加而信心沒有跟著上升、或要為生成的測試挑一個不會被灌水的門檻時，用來決定量什麼，以及那個量的盲區在哪裡">品質閘門的更替</a></li>
<li>術語卡 → <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">Test Oracle</a>、<a href="/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">Property-Based Testing</a>、<a href="/blog/testing/knowledge-cards/metamorphic-testing/" data-link-title="Metamorphic Testing（變形測試）" data-link-desc="被測對象沒有已知正確輸出、連不變量都難以陳述時，用兩次執行之間的關係取代預期值">Metamorphic Testing</a>、<a href="/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">Testing vs Checking</a></li>
<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></li>
</ul>
]]></content:encoded></item></channel></rss>