<?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>Test-Oracle on Tarragon</title><link>https://tarrragon.github.io/blog/tags/test-oracle/</link><description>Recent content in Test-Oracle 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/test-oracle/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/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><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>Test Oracle（測試判準來源）</title><link>https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/</guid><description>&lt;p>Test oracle 是測試判定「這次執行算通過還是算失敗」所依據的判準來源。斷言只是 oracle 的表達形式，真正的問題是那個預期值從哪裡取得——取得的路徑決定這個測試能發現什麼。規格給的 oracle 抓得出實作偏離需求；&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> 用現狀當 oracle，只抓得出行為改變；而 &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/stub/" data-link-title="Stub" data-link-desc="測試作者手動寫死回應資料的 test double：驗證的是假設成立時邏輯是否正確，假設本身錯誤時無法檢出">stub&lt;/a> 餵進去的資料若同時也是斷言的依據，這個測試沒有 oracle 可言，它驗證的是自己寫下的假設。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>測試三層（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>）回答的是驗證發生在哪一層，oracle 回答的是憑什麼判定，兩者正交：同一層的兩個測試可以有完全不同的判準來源，而判準來源決定了紅燈的意義。&lt;/p>
&lt;p>判準來源分兩層。第一層問&lt;strong>預期值從哪裡取得&lt;/strong>，有四種取法；取不到答案時進第二層，改問&lt;strong>預期值必須滿足什麼&lt;/strong>（不變量）或&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="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候&lt;/a>。&lt;/p>
&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;a href="https://tarrragon.github.io/blog/record/bdd-testing-methodology/" data-link-title="BDD 測試：測行為，不測實作細節" data-link-desc="替換一個實作就要跟著改掉大批測試、而業務邏輯根本沒動時，用來判斷哪一層該用 Given-When-Then、Mock 的邊界畫在哪裡">BDD 測行為不測實作&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;/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;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>Oracle problem 指的是被測對象沒有可直接取得的預期輸出這個處境。典型場合是最佳化演算法（知道解合不合法、算不出最佳解該長什麼樣）、編譯器與轉譯器（輸出是另一份程式）、繪圖與版面計算（正確與否是視覺判斷），以及規格只存在於人腦裡而從未被寫下的功能。這些場合逐例斷言寫不出來，判準要換成&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>另一種訊號來自紅燈的歧義：一個測試變紅時如果分不出「這代表行為改了」還是「這代表行為不對」，通常是同一個檔案裡混用了規格 oracle 與現狀 oracle。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>每個測試的作者要能指出預期值是從哪裡取得的。指不出來時最常見的實情是預期值取自實作的執行結果——先跑一次、把印出來的值貼進斷言，那是用實作驗證實作，不是驗證。這個退化在測試與實作由同一次生成產出時是預設會發生的事，判定方式見 &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;/p>
&lt;p>Oracle 的可信度是測試套件的上限：斷言寫得再細密，也不會超過判準來源本身的正確性。這條上限也劃出了自動化的邊界——判準無法事先寫下的驗證工作屬於&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></description><content:encoded><![CDATA[<p>Test oracle 是測試判定「這次執行算通過還是算失敗」所依據的判準來源。斷言只是 oracle 的表達形式，真正的問題是那個預期值從哪裡取得——取得的路徑決定這個測試能發現什麼。規格給的 oracle 抓得出實作偏離需求；<a href="/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">characterization test</a> 用現狀當 oracle，只抓得出行為改變；而 <a href="/blog/testing/knowledge-cards/stub/" data-link-title="Stub" data-link-desc="測試作者手動寫死回應資料的 test double：驗證的是假設成立時邏輯是否正確，假設本身錯誤時無法檢出">stub</a> 餵進去的資料若同時也是斷言的依據，這個測試沒有 oracle 可言，它驗證的是自己寫下的假設。</p>
<h2 id="概念位置">概念位置</h2>
<p>測試三層（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>）回答的是驗證發生在哪一層，oracle 回答的是憑什麼判定，兩者正交：同一層的兩個測試可以有完全不同的判準來源，而判準來源決定了紅燈的意義。</p>
<p>判準來源分兩層。第一層問<strong>預期值從哪裡取得</strong>，有四種取法；取不到答案時進第二層，改問<strong>預期值必須滿足什麼</strong>（不變量）或<strong>輸入變了輸出該怎麼變</strong>（變形關係）——那兩種是判準的替代形態而不是取得預期值的來源，退階的完整判準在<a href="/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候</a>。</p>
<p>第一層四種來源各自的覆蓋範圍：</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>預期值從哪來</th>
          <th>抓得到什麼</th>
          <th>抓不到什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規格</td>
          <td>人寫下的需求</td>
          <td>實作與需求之間的偏離</td>
          <td>需求本身寫錯了（那一層走 <a href="/blog/record/bdd-testing-methodology/" data-link-title="BDD 測試：測行為，不測實作細節" data-link-desc="替換一個實作就要跟著改掉大批測試、而業務邏輯根本沒動時，用來判斷哪一層該用 Given-When-Then、Mock 的邊界畫在哪裡">BDD 測行為不測實作</a>）</td>
      </tr>
      <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>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>Oracle problem 指的是被測對象沒有可直接取得的預期輸出這個處境。典型場合是最佳化演算法（知道解合不合法、算不出最佳解該長什麼樣）、編譯器與轉譯器（輸出是另一份程式）、繪圖與版面計算（正確與否是視覺判斷），以及規格只存在於人腦裡而從未被寫下的功能。這些場合逐例斷言寫不出來，判準要換成<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>另一種訊號來自紅燈的歧義：一個測試變紅時如果分不出「這代表行為改了」還是「這代表行為不對」，通常是同一個檔案裡混用了規格 oracle 與現狀 oracle。</p>
<h2 id="設計責任">設計責任</h2>
<p>每個測試的作者要能指出預期值是從哪裡取得的。指不出來時最常見的實情是預期值取自實作的執行結果——先跑一次、把印出來的值貼進斷言，那是用實作驗證實作，不是驗證。這個退化在測試與實作由同一次生成產出時是預設會發生的事，判定方式見 <a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">test provenance</a>。</p>
<p>Oracle 的可信度是測試套件的上限：斷言寫得再細密，也不會超過判準來源本身的正確性。這條上限也劃出了自動化的邊界——判準無法事先寫下的驗證工作屬於<a href="/blog/testing/knowledge-cards/testing-vs-checking/" data-link-title="Testing vs Checking（探究與檢查）" data-link-desc="斷言可以被整批產生之後，用來區分哪些驗證工作交得出去給機器、哪些需要人的判斷才成立">探究而非檢查</a>。</p>
]]></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><item><title>Property-Based Testing（性質式測試）</title><link>https://tarrragon.github.io/blog/testing/knowledge-cards/property-based-testing/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/knowledge-cards/property-based-testing/</guid><description>&lt;p>一條對所有合法輸入都該成立的性質，就是 property-based testing 拿來當斷言的東西——例如「排序後的長度與原本相同」（守恆）、「編碼再解碼會得到原值」（往返）、「折扣後的金額不會超過原價」（上下界）。框架負責生成大量輸入去試圖推翻它，並在找到反例時自動縮小到最短的那一個。它跟逐例測試的差別在 &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;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>不變量是比例子更難被實作污染的一種判準。逐例的預期值可以從實作跑一次抄回來，不變量抄不了——它必須從需求推導。這讓性質式測試在測試與實作&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;p>四類骨幹性質（另有上下界與順序無關兩類，連同適用條件與划算判準在&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;/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;/tbody>
&lt;/table>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>同一個測試被複製貼上多次、只有輸入的數值不同：那組例子背後藏著一條沒被寫出來的性質，把它寫出來就是這類測試的起點。另一個訊號是邊界值列表越補越長：0、1、-1、最大值、空字串、單一元素，每次線上出問題就補一個，代表判準是靠回憶累積的。&lt;/p>
&lt;p>失敗的縮小結果本身是產出。框架回報的最短反例通常直接就是缺陷的最小重現案例，可以原樣固定成一個逐例測試，讓這次的具體失敗有一個穩定的回歸點。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>寫得出來的性質決定了這個測試的射程，所以性質太弱是主要的失效方式。「結果不會是 null」對所有實作都成立，包含錯的那些；把它收緊成「結果的長度等於輸入的長度且每個元素都來自輸入」才開始有鑑別力。判斷方式跟&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;p>生成器要跟著資料的真實形狀走。預設生成器產出的字串多半是短的隨機字元，而實際輸入可能有多位元組字元、前後空白與極長的內容；生成器沒有涵蓋的形狀，性質再強也驗不到，這與 &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;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/metamorphic-testing/" data-link-title="Metamorphic Testing（變形測試）" data-link-desc="被測對象沒有已知正確輸出、連不變量都難以陳述時，用兩次執行之間的關係取代預期值">變形關係&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>一條對所有合法輸入都該成立的性質，就是 property-based testing 拿來當斷言的東西——例如「排序後的長度與原本相同」（守恆）、「編碼再解碼會得到原值」（往返）、「折扣後的金額不會超過原價」（上下界）。框架負責生成大量輸入去試圖推翻它，並在找到反例時自動縮小到最短的那一個。它跟逐例測試的差別在 <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle</a> 的來源——逐例測試要人算出每個輸入對應的答案，性質式測試只要人寫下答案必須滿足的條件，而條件通常比答案好寫。</p>
<h2 id="概念位置">概念位置</h2>
<p>不變量是比例子更難被實作污染的一種判準。逐例的預期值可以從實作跑一次抄回來，不變量抄不了——它必須從需求推導。這讓性質式測試在測試與實作<a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">出處不獨立</a>的情況下保有較多驗證力，而生成器的存在也讓它涵蓋到人不會主動想到的輸入組合。</p>
<p>四類骨幹性質（另有上下界與順序無關兩類，連同適用條件與划算判準在<a href="/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候</a>）：</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>
  </tbody>
</table>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>同一個測試被複製貼上多次、只有輸入的數值不同：那組例子背後藏著一條沒被寫出來的性質，把它寫出來就是這類測試的起點。另一個訊號是邊界值列表越補越長：0、1、-1、最大值、空字串、單一元素，每次線上出問題就補一個，代表判準是靠回憶累積的。</p>
<p>失敗的縮小結果本身是產出。框架回報的最短反例通常直接就是缺陷的最小重現案例，可以原樣固定成一個逐例測試，讓這次的具體失敗有一個穩定的回歸點。</p>
<h2 id="設計責任">設計責任</h2>
<p>寫得出來的性質決定了這個測試的射程，所以性質太弱是主要的失效方式。「結果不會是 null」對所有實作都成立，包含錯的那些；把它收緊成「結果的長度等於輸入的長度且每個元素都來自輸入」才開始有鑑別力。判斷方式跟<a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">突變測試</a>一致——問這條性質擋不擋得住某個具體的錯誤寫法。</p>
<p>生成器要跟著資料的真實形狀走。預設生成器產出的字串多半是短的隨機字元，而實際輸入可能有多位元組字元、前後空白與極長的內容；生成器沒有涵蓋的形狀，性質再強也驗不到，這與 <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>是同一個問題。連性質都難以陳述時，判準要再退一層到<a href="/blog/testing/knowledge-cards/metamorphic-testing/" data-link-title="Metamorphic Testing（變形測試）" data-link-desc="被測對象沒有已知正確輸出、連不變量都難以陳述時，用兩次執行之間的關係取代預期值">變形關係</a>。</p>
]]></content:encoded></item><item><title>Metamorphic Testing（變形測試）</title><link>https://tarrragon.github.io/blog/testing/knowledge-cards/metamorphic-testing/</link><pubDate>Mon, 24 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/knowledge-cards/metamorphic-testing/</guid><description>&lt;p>把輸入依照某條規則改變，輸出應該依照對應的規則改變——metamorphic testing 驗的就是這組對應，而不是任何一次執行的答案。搜尋條件加上一個更嚴格的過濾，結果集應該是原本的子集；把所有商品的價格同乘以二，總價也該同乘以二；在圖片上加一點雜訊，分類結果不該翻轉。這條「輸入怎麼變、輸出就該怎麼變」的對應稱為變形關係，它讓測試在沒有任何一次執行的正確答案可查的情況下仍然成立——這是繞開 &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle problem&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="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">不變量&lt;/a>的要求更低。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>三種判準的要求由強到弱排成一條線：逐例斷言要知道每個輸入的正確輸出，不變量只要知道輸出必須滿足什麼，變形關係連這個都不需要、只要知道輸入變了輸出該怎麼變。變形關係因此是三者裡門檻最低、也是最後一道還寫得下來的判準。完整的退階表與選用程序在&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;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;/p>
&lt;p>它跟參照實作 oracle 的差別在於不需要第二份實作：變形關係比較的是同一份實作的兩次執行，因此在只有一個實作可用的場景（多數自研系統）仍然適用。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>「跑出來的結果對不對，沒有人說得準；但如果它變成那樣，就一定是錯的」——講得出這句話的時候就到了變形關係的位置。推薦排序、搜尋相關度、路徑規劃、稅務與費率試算、以及模型輸出的穩定性，都落在這一類——最終答案的正確性難以逐例判定，而輸入輕微改動後輸出翻轉是可以斷定的異常。&lt;/p>
&lt;p>關係的方向要跟著業務語意選。搜尋條件收緊時結果集該縮小是子集關係；商品清單順序打亂時結果集該完全相同是置換不變關係；兩者同時成立，而各自抓的是不同的缺陷——前者抓過濾邏輯，後者抓對輸入順序的隱性依賴。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>變形關係要從需求推導，不是從實作觀察。從實作跑兩次看到某個對應成立就把它寫成斷言，等於把實作現在的行為當成規格，這與&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;p>關係被破壞時的歸因需要額外一步。逐例斷言變紅指向一個具體的輸出值，變形關係變紅只說明兩次執行的關係不對，缺陷可能在任一次執行裡；縮小範圍的做法是把一條複合關係拆成幾條單步關係，讓每次只改變一個輸入維度。&lt;/p></description><content:encoded><![CDATA[<p>把輸入依照某條規則改變，輸出應該依照對應的規則改變——metamorphic testing 驗的就是這組對應，而不是任何一次執行的答案。搜尋條件加上一個更嚴格的過濾，結果集應該是原本的子集；把所有商品的價格同乘以二，總價也該同乘以二；在圖片上加一點雜訊，分類結果不該翻轉。這條「輸入怎麼變、輸出就該怎麼變」的對應稱為變形關係，它讓測試在沒有任何一次執行的正確答案可查的情況下仍然成立——這是繞開 <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">oracle problem</a> 最後一層的手段，比<a href="/blog/testing/knowledge-cards/property-based-testing/" data-link-title="Property-Based Testing（性質式測試）" data-link-desc="逐例斷言列不完、或預期輸出難以逐例算出時，用來把驗證對象從個別例子換成對所有輸入都該成立的性質">不變量</a>的要求更低。</p>
<h2 id="概念位置">概念位置</h2>
<p>三種判準的要求由強到弱排成一條線：逐例斷言要知道每個輸入的正確輸出，不變量只要知道輸出必須滿足什麼，變形關係連這個都不需要、只要知道輸入變了輸出該怎麼變。變形關係因此是三者裡門檻最低、也是最後一道還寫得下來的判準。完整的退階表與選用程序在<a href="/blog/testing/06-agent-authored-code/oracle-beyond-examples/" data-link-title="判準寫不下來的時候：性質、變形關係與留給人的部分" data-link-desc="逐例預期值算不出來或跟不上產出速度時，用來決定判準退到哪一層、以及哪些驗證工作交不出去">判準寫不下來的時候</a>，來源分類見 <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle（測試判準來源）" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判準的來源，以及每一種來源的射程">test oracle</a>。</p>
<p>它跟參照實作 oracle 的差別在於不需要第二份實作：變形關係比較的是同一份實作的兩次執行，因此在只有一個實作可用的場景（多數自研系統）仍然適用。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>「跑出來的結果對不對，沒有人說得準；但如果它變成那樣，就一定是錯的」——講得出這句話的時候就到了變形關係的位置。推薦排序、搜尋相關度、路徑規劃、稅務與費率試算、以及模型輸出的穩定性，都落在這一類——最終答案的正確性難以逐例判定，而輸入輕微改動後輸出翻轉是可以斷定的異常。</p>
<p>關係的方向要跟著業務語意選。搜尋條件收緊時結果集該縮小是子集關係；商品清單順序打亂時結果集該完全相同是置換不變關係；兩者同時成立，而各自抓的是不同的缺陷——前者抓過濾邏輯，後者抓對輸入順序的隱性依賴。</p>
<h2 id="設計責任">設計責任</h2>
<p>變形關係要從需求推導，不是從實作觀察。從實作跑兩次看到某個對應成立就把它寫成斷言，等於把實作現在的行為當成規格，這與<a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">出處不獨立</a>的失效是同一件事；正確的來源是業務語意上「本來就該如此」的那些對應。</p>
<p>關係被破壞時的歸因需要額外一步。逐例斷言變紅指向一個具體的輸出值，變形關係變紅只說明兩次執行的關係不對，缺陷可能在任一次執行裡；縮小範圍的做法是把一條複合關係拆成幾條單步關係，讓每次只改變一個輸入維度。</p>
]]></content:encoded></item></channel></rss>