<?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>Property-Based-Testing on Tarragon</title><link>https://tarrragon.github.io/blog/tags/property-based-testing/</link><description>Recent content in Property-Based-Testing 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/property-based-testing/index.xml" rel="self" type="application/rss+xml"/><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>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></channel></rss>