<?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>Characterization-Test on Tarragon</title><link>https://tarrragon.github.io/blog/tags/characterization-test/</link><description>Recent content in Characterization-Test on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 17 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/characterization-test/index.xml" rel="self" type="application/rss+xml"/><item><title>無測試 legacy 專案的起步順序</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/legacy-test-bootstrap/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/legacy-test-bootstrap/</guid><description>&lt;p>接手一個沒有任何測試的專案，團隊能投入的測試時間有限。第一條測試寫什麼，決定的是接下來幾個月的投資回報走向——寫對了，每條新測試都在降低最高風險區的暴露面；寫錯了，測試數量在增長但攔截能力沒有對準系統的脆弱處。&lt;/p>
&lt;p>這裡回答的是「投資順序」。值不值得在既有程式碼上加測試（相對於重寫）是更上游的判斷，&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試&lt;/a>的「結構界限與適用判準」段有討論不同規模的取捨。&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>——每層的投資回報取決於風險集中在哪裡。照測試金字塔從 unit 開始往上疊是一種預設路徑，但 legacy 專案的風險分布很少均勻：某些系統的風險集中在內部邏輯、某些集中在外部互動、某些集中在流程編排。起步層對準風險集中處，每條測試的邊際收益最高。&lt;/p>
&lt;p>三個自查問句幫助定位風險集中處：&lt;/p>
&lt;h3 id="過去半年的-bug-報告集中在算錯還是連錯">過去半年的 bug 報告集中在「算錯」還是「連錯」？&lt;/h3>
&lt;ul>
&lt;li>算錯（金額計算、狀態轉換、資料轉換的錯誤）→ 風險在內部邏輯，unit test 的攔截面最大&lt;/li>
&lt;li>連錯（API 回應格式改了沒跟上、認證握手失敗、訊息格式不相容）→ 風險在外部互動，protocol integration test 的攔截面最大&lt;/li>
&lt;/ul>
&lt;h3 id="系統的正確性有多大程度取決於我們對外部服務行為的假設">系統的正確性有多大程度取決於「我們對外部服務行為的假設」？&lt;/h3>
&lt;ul>
&lt;li>系統主要依賴自己的計算和規則 → unit test 先行&lt;/li>
&lt;li>系統的核心價值在「跟第三方服務的互動」（支付閘道、物流 API、認證服務）→ protocol integration test 先行，確保互動契約正確&lt;/li>
&lt;/ul>
&lt;h3 id="bug-是在哪個層面被使用者發現的">Bug 是在哪個層面被使用者發現的？&lt;/h3>
&lt;ul>
&lt;li>使用者回報的是「按了按鈕沒反應」「畫面顯示的跟預期不同」→ 風險在 UI 行為與導航，screen state 驗證先行&lt;/li>
&lt;li>使用者回報的是「資料不對」「扣款金額錯」→ 風險在底層邏輯，往 unit 或 protocol integration 定位&lt;/li>
&lt;/ul>
&lt;p>三個問句不互斥——一個系統可能同時在內部邏輯和外部互動都有高風險。此時選風險造成的業務損害更大的那一端先投資。&lt;/p>
&lt;p>第三條問句指向的 &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 test&lt;/a> 先行路徑沒有列在下面的三條路徑裡，因為它的建置前提與前三條不同——需要 UI 測試框架（widget test / Playwright）已能在專案中執行。若風險確實集中在畫面狀態的完整性（狀態轉換遺漏、佔位頁面殘留），以&lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">狀態矩陣&lt;/a>為 test case 來源是最直接的起步形態。&lt;/p>
&lt;h2 id="三種起步路徑">三種起步路徑&lt;/h2>
&lt;h3 id="路徑一unit-test-先行">路徑一：Unit test 先行&lt;/h3>
&lt;p>bug 報告集中在「算錯」（金額、狀態、轉換），而且這些邏輯的輸入輸出邊界可辨識——函式簽名清楚、依賴可注入或可隔離。&lt;/p>
&lt;p>&lt;strong>起步策略&lt;/strong>：從失敗代價最高的業務邏輯開始。不是從最容易測的函式開始——最容易測的通常是工具函式（字串處理、日期轉換），它們的失敗代價低。找到「算錯了會直接造成客訴或財損」的邏輯區塊，從那裡寫第一批 unit test。&lt;/p>
&lt;p>&lt;strong>常見阻力與對策&lt;/strong>：legacy 程式碼的依賴常常是纏結的（一個計價函式裡面直接呼叫資料庫、呼叫外部 API、讀取全域狀態）。逐步解耦是理想做法，但前幾條測試不需要完美的依賴注入——用 monkey patching、module-level mock、或 subclass override 先把依賴隔開，換到第一批測試能跑。架構層面的重構在有測試保護之後再做，順序反過來（先重構再測試）會讓重構本身失去安全網。&lt;/p>
&lt;p>&lt;strong>接續路由&lt;/strong>：unit test 建立起對內部邏輯的攔截後，下一步判斷是否需要 &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">protocol integration test&lt;/a> 補上外部互動的盲區。判準：&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">mock 遮蔽機制&lt;/a>描述的哪些問題在這個專案可能成立。&lt;/p>
&lt;h3 id="路徑二protocol-integration-test-先行">路徑二：Protocol integration test 先行&lt;/h3>
&lt;p>系統的核心價值在整合——聚合多個外部服務的資料、轉發訊息到不同通道、對第三方 API 做操作。內部邏輯相對薄（主要是組請求和轉格式），風險集中在「跟外部服務的契約是否正確」。&lt;/p>
&lt;p>&lt;strong>起步策略&lt;/strong>：盤點系統連接的所有外部服務，按兩個維度排序——變動頻率和失敗影響。變動頻率高（API 版本升級快、回應格式常改）且失敗影響大（付款、認證、核心資料同步）的服務，優先寫 protocol integration test。&lt;/p>
&lt;p>&lt;strong>可行性前提&lt;/strong>：外部服務需要一個可測試的環境——sandbox、staging、或可在本機啟動的服務實例。如果外部服務沒有測試環境（某些第三方 API 只提供生產環境），protocol integration test 的成本會大幅上升；這時候先用 &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/http-contract-test/" data-link-title="HTTP contract test 設計" data-link-desc="HTTP REST API 的 protocol integration test — request/response 格式、status code 語意、error body 結構的驗證">HTTP contract test&lt;/a> 對著 API 文件寫 schema 驗證，以契約測試替代。&lt;/p></description><content:encoded><![CDATA[<p>接手一個沒有任何測試的專案，團隊能投入的測試時間有限。第一條測試寫什麼，決定的是接下來幾個月的投資回報走向——寫對了，每條新測試都在降低最高風險區的暴露面；寫錯了，測試數量在增長但攔截能力沒有對準系統的脆弱處。</p>
<p>這裡回答的是「投資順序」。值不值得在既有程式碼上加測試（相對於重寫）是更上游的判斷，<a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a>的「結構界限與適用判準」段有討論不同規模的取捨。</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>——每層的投資回報取決於風險集中在哪裡。照測試金字塔從 unit 開始往上疊是一種預設路徑，但 legacy 專案的風險分布很少均勻：某些系統的風險集中在內部邏輯、某些集中在外部互動、某些集中在流程編排。起步層對準風險集中處，每條測試的邊際收益最高。</p>
<p>三個自查問句幫助定位風險集中處：</p>
<h3 id="過去半年的-bug-報告集中在算錯還是連錯">過去半年的 bug 報告集中在「算錯」還是「連錯」？</h3>
<ul>
<li>算錯（金額計算、狀態轉換、資料轉換的錯誤）→ 風險在內部邏輯，unit test 的攔截面最大</li>
<li>連錯（API 回應格式改了沒跟上、認證握手失敗、訊息格式不相容）→ 風險在外部互動，protocol integration test 的攔截面最大</li>
</ul>
<h3 id="系統的正確性有多大程度取決於我們對外部服務行為的假設">系統的正確性有多大程度取決於「我們對外部服務行為的假設」？</h3>
<ul>
<li>系統主要依賴自己的計算和規則 → unit test 先行</li>
<li>系統的核心價值在「跟第三方服務的互動」（支付閘道、物流 API、認證服務）→ protocol integration test 先行，確保互動契約正確</li>
</ul>
<h3 id="bug-是在哪個層面被使用者發現的">Bug 是在哪個層面被使用者發現的？</h3>
<ul>
<li>使用者回報的是「按了按鈕沒反應」「畫面顯示的跟預期不同」→ 風險在 UI 行為與導航，screen state 驗證先行</li>
<li>使用者回報的是「資料不對」「扣款金額錯」→ 風險在底層邏輯，往 unit 或 protocol integration 定位</li>
</ul>
<p>三個問句不互斥——一個系統可能同時在內部邏輯和外部互動都有高風險。此時選風險造成的業務損害更大的那一端先投資。</p>
<p>第三條問句指向的 <a href="/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">screen state test</a> 先行路徑沒有列在下面的三條路徑裡，因為它的建置前提與前三條不同——需要 UI 測試框架（widget test / Playwright）已能在專案中執行。若風險確實集中在畫面狀態的完整性（狀態轉換遺漏、佔位頁面殘留），以<a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">狀態矩陣</a>為 test case 來源是最直接的起步形態。</p>
<h2 id="三種起步路徑">三種起步路徑</h2>
<h3 id="路徑一unit-test-先行">路徑一：Unit test 先行</h3>
<p>bug 報告集中在「算錯」（金額、狀態、轉換），而且這些邏輯的輸入輸出邊界可辨識——函式簽名清楚、依賴可注入或可隔離。</p>
<p><strong>起步策略</strong>：從失敗代價最高的業務邏輯開始。不是從最容易測的函式開始——最容易測的通常是工具函式（字串處理、日期轉換），它們的失敗代價低。找到「算錯了會直接造成客訴或財損」的邏輯區塊，從那裡寫第一批 unit test。</p>
<p><strong>常見阻力與對策</strong>：legacy 程式碼的依賴常常是纏結的（一個計價函式裡面直接呼叫資料庫、呼叫外部 API、讀取全域狀態）。逐步解耦是理想做法，但前幾條測試不需要完美的依賴注入——用 monkey patching、module-level mock、或 subclass override 先把依賴隔開，換到第一批測試能跑。架構層面的重構在有測試保護之後再做，順序反過來（先重構再測試）會讓重構本身失去安全網。</p>
<p><strong>接續路由</strong>：unit test 建立起對內部邏輯的攔截後，下一步判斷是否需要 <a href="/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">protocol integration test</a> 補上外部互動的盲區。判準：<a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">mock 遮蔽機制</a>描述的哪些問題在這個專案可能成立。</p>
<h3 id="路徑二protocol-integration-test-先行">路徑二：Protocol integration test 先行</h3>
<p>系統的核心價值在整合——聚合多個外部服務的資料、轉發訊息到不同通道、對第三方 API 做操作。內部邏輯相對薄（主要是組請求和轉格式），風險集中在「跟外部服務的契約是否正確」。</p>
<p><strong>起步策略</strong>：盤點系統連接的所有外部服務，按兩個維度排序——變動頻率和失敗影響。變動頻率高（API 版本升級快、回應格式常改）且失敗影響大（付款、認證、核心資料同步）的服務，優先寫 protocol integration test。</p>
<p><strong>可行性前提</strong>：外部服務需要一個可測試的環境——sandbox、staging、或可在本機啟動的服務實例。如果外部服務沒有測試環境（某些第三方 API 只提供生產環境），protocol integration test 的成本會大幅上升；這時候先用 <a href="/blog/testing/03-protocol-integration-test/http-contract-test/" data-link-title="HTTP contract test 設計" data-link-desc="HTTP REST API 的 protocol integration test — request/response 格式、status code 語意、error body 結構的驗證">HTTP contract test</a> 對著 API 文件寫 schema 驗證，以契約測試替代。</p>
<p><strong>接續路由</strong>：protocol integration test 確認外部契約後，回頭用 unit test 補內部邏輯的覆蓋。測試策略的<a href="/blog/testing/01-test-strategy-layers/three-layer-definition/" data-link-title="三層定義與職責表" data-link-desc="Unit Test / Protocol Integration Test / Screen State Test 各層職責、驗證目標與盲區的完整論述">三層定義</a>描述了每層的職責與盲區。</p>
<h3 id="路徑三流程測試先行">路徑三：流程測試先行</h3>
<p>出錯頻率最高的功能橫跨多個服務的接力——前端收到使用者輸入後，經過驗證、計算、外部呼叫、狀態更新、通知，最終結果正確要求整條鏈都正確。單獨測任何一層，攔截率都偏低。</p>
<p><strong>起步策略</strong>：找到一條「出錯頻率最高」或「出錯後果最嚴重」的業務流程，把它寫成一個端到端的流程測試——從編排入口驅動整條服務鏈。如果被測編排可以從 UI 層抽離（<a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a>的「可測性閘門」段描述了判定方法），走 subcutaneous test 形態；抽不出來就走 E2E。</p>
<p><strong>代價意識</strong>：流程測試的建置和維護成本高於 unit test 和 protocol integration test。Legacy 專案的編排通常跟 UI 框架、全域狀態、平台依賴交織在一起，拆開接縫的前置工程量大。撤退訊號用結構性阻力判斷而非時間倍數：拆接縫時發現需要改動的生產模組超過三個、或第一條 spike 需要的假件數超過五個，代表耦合度超出流程測試先行的成本預期——回到路徑一或路徑二先建低成本的攔截，有部分覆蓋比沒有覆蓋好。這兩個數字是經驗閾值：改動三個以上模組代表波及面超出單一功能的接縫、spike 工期從天級跳到週級；假件數超過五個代表測試的假設比被測邏輯還多。團隊可依自身的模組粒度調整。</p>
<p><strong>接續路由</strong>：流程測試建立後，用 unit test 覆蓋流程中各段服務的內部邏輯——流程測試紅燈時，有 unit test 才能快速定位是哪一段出問題。這條接續不是可選的收尾：流程測試先行換到的是攔截率，代價是除錯半徑橫跨整條服務鏈、定位成本高於單元測試。細粒度回饋被延後、不是被放棄，延後期間的定位成本就是這條路徑的已知帳單。定位成本的討論見<a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a>的「結構界限與適用判準」段。</p>
<h2 id="遷移安全網先有-characterization-test-再改">遷移安全網：先有 characterization test 再改</h2>
<p>三種路徑都涉及一個共同的操作風險：在沒有測試保護的程式碼上做改動（加測試本身常常需要調整程式碼結構——提取函式、注入依賴、暴露接縫）。<a href="/blog/testing/knowledge-cards/characterization-test/" data-link-title="Characterization Test" data-link-desc="斷言「行為不變」而非「行為正確」的測試形態；與正確性測試的語意分界，決定紅燈能不能歸因">Characterization test</a> 是這個操作的安全網。</p>
<p>Characterization test 鎖住的是「現在的行為」，不關心行為是否正確。寫法是對著現有程式碼跑一次、把輸出記錄下來當預期值。改動後全綠代表「行為沒變」，紅燈代表「行為改了」——紅燈不一定是 bug，但它告訴你改動的影響範圍。</p>
<p>這個工具在 legacy 專案起步階段的價值在於降低重構風險：要把纏結的依賴拆開才能寫 unit test，但拆的過程可能改壞現有行為——characterization test 先鎖住行為，讓拆解有安全網。拆完、unit test 寫好後，characterization test 的歷史使命完成，可以視重疊程度逐步移除。兩者的職責分界：characterization test 斷言「行為不變」，unit test 斷言「行為正確」——混在一起會讓紅燈無法歸因是「行為改了」還是「行為本來就錯」。實作經驗與平台依賴的處理見 <a href="/blog/work-log/flutter_characterization_test_migration_safety_net/" data-link-title="測「不變」、不測「正確」 — characterization test 當遷移安全網" data-link-desc="大規模型別遷移前，對著舊實作寫一批鎖住現有行為的測試——包括看起來像 bug 的邊界怪癖也照鎖，遷移後全綠證明「換底沒改行為」。正確性是另一批測試的職責、混在一起紅燈就無法歸因。附測試環境的原生依賴三個斷點（FFI、plugin、late init）與替身解法。">characterization test 當遷移安全網</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>測試三層各自的職責與盲區 → <a href="/blog/testing/01-test-strategy-layers/three-layer-definition/" data-link-title="三層定義與職責表" data-link-desc="Unit Test / Protocol Integration Test / Screen State Test 各層職責、驗證目標與盲區的完整論述">測試策略三層定義</a></li>
<li>Mock 遮蔽機制（為什麼 unit test 有結構性盲區）→ <a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">Mock 遮蔽機制</a></li>
<li>語意級假後端的適用判準 → <a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a></li>
<li>Characterization test 實作經驗 → <a href="/blog/work-log/flutter_characterization_test_migration_safety_net/" data-link-title="測「不變」、不測「正確」 — characterization test 當遷移安全網" data-link-desc="大規模型別遷移前，對著舊實作寫一批鎖住現有行為的測試——包括看起來像 bug 的邊界怪癖也照鎖，遷移後全綠證明「換底沒改行為」。正確性是另一批測試的職責、混在一起紅燈就無法歸因。附測試環境的原生依賴三個斷點（FFI、plugin、late init）與替身解法。">characterization test 當遷移安全網</a></li>
<li>Protocol integration test 的定義與邊界 → <a href="/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">Protocol integration test 定義</a></li>
<li>起步層選定後、單條測試該測什麼（未來哪種改壞要被擋）→ <a href="/blog/testing/05-test-design-judgment/test-as-change-guard/" data-link-title="測試的價值發生在它變紅的那一刻——建立、變更與註解分工的防護視角" data-link-desc="為一段邏輯決定第一條測試該測什麼、變更或重構前評估既有測試會擋什麼、review 裡爭論某個約束該寫註解還是測試、或拿到紅燈要判讀它是刻意規則還是漏網 bug 時使用。這些問題共用同一個視角：測試的價值在未來的紅燈、不在當下的綠燈。">測試的價值發生在它變紅的那一刻</a></li>
</ul>
]]></content:encoded></item><item><title>Characterization Test</title><link>https://tarrragon.github.io/blog/testing/knowledge-cards/characterization-test/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/testing/knowledge-cards/characterization-test/</guid><description>&lt;p>Characterization test 的斷言對象是現狀本身：預期值取自被測程式碼當下的實際輸出，而非規格說它該輸出什麼。這個語意讓它的紅燈只有一種意義——行為改了。正確性測試的紅燈則有兩種意義（行為改了、或行為本來就不對），兩者混在同一個測試檔裡，紅燈就失去歸因能力。與測試三層（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>）的關係是正交：三層各自驗證正確性，characterization test 守護現狀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>當依賴纏結到必須先拆解才能寫正確性測試、而拆解本身就可能改壞行為時，characterization test 補上這段真空期的回饋——這是它唯一的存在理由。依賴纏結的典型樣貌是&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock Masking（Mock 遮蔽）" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽&lt;/a>描述的那種處境：程式碼直接呼叫外部服務、沒有可注入的介面。這個處境在 &lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/legacy-test-bootstrap/" data-link-title="無測試 legacy 專案的起步順序" data-link-desc="接手零測試的專案、有限預算下第一批測試該從哪一層開始建——按風險集中處判斷起步路徑，而非照測試金字塔從底部往上疊">legacy 專案的起步順序&lt;/a>的「遷移安全網」段有完整的操作判準——包含三種起步路徑各自何時需要它。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>改動範圍不可知時——沒有測試、依賴纏結、沒人記得它該回傳什麼——先鎖行為再動手；範圍可知時（規格明確、依賴已可注入）直接寫正確性測試更划算。判斷落點就在這條分岔上：它的價值來自不確定性，不確定性消失，它就失去存在理由。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>這類測試自帶退場條件，維護者要負責執行它：正確性測試覆蓋同一段行為後，characterization test 的使命結束、應該移除。留著不移除的代價是雙份維護，且兩份測試對同一行為的斷言可能分岔。與 &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock Masking（Mock 遮蔽）" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽&lt;/a>的關係值得留意——characterization test 記錄的是含既有 bug 的行為，它保證的是「沒改壞」，不保證「是對的」。&lt;/p>
&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></description><content:encoded><![CDATA[<p>Characterization test 的斷言對象是現狀本身：預期值取自被測程式碼當下的實際輸出，而非規格說它該輸出什麼。這個語意讓它的紅燈只有一種意義——行為改了。正確性測試的紅燈則有兩種意義（行為改了、或行為本來就不對），兩者混在同一個測試檔裡，紅燈就失去歸因能力。與測試三層（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>）的關係是正交：三層各自驗證正確性，characterization test 守護現狀。</p>
<h2 id="概念位置">概念位置</h2>
<p>當依賴纏結到必須先拆解才能寫正確性測試、而拆解本身就可能改壞行為時，characterization test 補上這段真空期的回饋——這是它唯一的存在理由。依賴纏結的典型樣貌是<a href="/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock Masking（Mock 遮蔽）" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽</a>描述的那種處境：程式碼直接呼叫外部服務、沒有可注入的介面。這個處境在 <a href="/blog/testing/01-test-strategy-layers/legacy-test-bootstrap/" data-link-title="無測試 legacy 專案的起步順序" data-link-desc="接手零測試的專案、有限預算下第一批測試該從哪一層開始建——按風險集中處判斷起步路徑，而非照測試金字塔從底部往上疊">legacy 專案的起步順序</a>的「遷移安全網」段有完整的操作判準——包含三種起步路徑各自何時需要它。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>改動範圍不可知時——沒有測試、依賴纏結、沒人記得它該回傳什麼——先鎖行為再動手；範圍可知時（規格明確、依賴已可注入）直接寫正確性測試更划算。判斷落點就在這條分岔上：它的價值來自不確定性，不確定性消失，它就失去存在理由。</p>
<h2 id="設計責任">設計責任</h2>
<p>這類測試自帶退場條件，維護者要負責執行它：正確性測試覆蓋同一段行為後，characterization test 的使命結束、應該移除。留著不移除的代價是雙份維護，且兩份測試對同一行為的斷言可能分岔。與 <a href="/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock Masking（Mock 遮蔽）" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽</a>的關係值得留意——characterization test 記錄的是含既有 bug 的行為，它保證的是「沒改壞」，不保證「是對的」。</p>
<p>這也讓它成為<a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">判準由實作推導</a>這件事在站內唯一的合法形態：預期值刻意取自被測程式的實際輸出，是設計選擇而不是失誤。代價要算清楚——它因此抓不到需求被誤讀那一類，保留下來的驗證力限於崩潰、回歸與內部不一致。把它當成正確性測試用，就是把一個刻意的取捨誤當成保證。</p>
]]></content:encoded></item><item><title>測「不變」、不測「正確」 — characterization test 當遷移安全網</title><link>https://tarrragon.github.io/blog/work-log/flutter_characterization_test_migration_safety_net/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/flutter_characterization_test_migration_safety_net/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：POS 專案要做金額型別的全面遷移（Decimal 換 Money extension type），動的是全部 model 的金額欄位。測試目錄裡出現一批名字帶 &lt;code>_characterization_test&lt;/code> 的檔案、開頭都有同一段宣告：「在 Money value object 遷移前建立、鎖住現有行為」
&lt;strong>疑問來源&lt;/strong>：這批測試跟一般的單元測試差在哪？為什麼連「找零算出負數歸零」這種邊界怪癖也被鎖進去？
&lt;strong>整理目的&lt;/strong>：記下 characterization test 的方法、跟正確性測試的分工、以及它依賴的測試環境前置（原生依賴的三個斷點）
&lt;strong>本文邊界&lt;/strong>：素材是該專案的五個 characterization 測試檔與 DEVLOG 的測試環境除錯史；characterization test 是 Michael Feathers 在 legacy code 領域的術語、這裡是它在型別遷移的應用&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="斷言的對象是現況包括現況的怪癖">斷言的對象是「現況」、包括現況的怪癖&lt;/h2>
&lt;p>characterization test 跟一般測試共用全部語法、差別只在斷言的依據：一般測試斷言&lt;strong>規格&lt;/strong>（找零應該是多少）、characterization test 斷言&lt;strong>現況&lt;/strong>（現在的實作算出多少）。寫法是對著舊實作跑一次、把輸出原樣釘進 expect——實作是測試的 oracle、不是規格。&lt;/p>
&lt;p>所以連怪癖也照鎖：現有實作在找零算出負數時歸零、這個行為對不對是另一回事，characterization test 把它鎖住。五個檔案的分佈跟著遷移的影響面走——checkout 的應付金額 fold 與現金找零、order 的小計、cart item 的價格計算、product spec 的三種價格——&lt;strong>遷移會經過的每條金額計算路徑、各有一張行為快照&lt;/strong>。&lt;/p>
&lt;p>分工的必要性在遷移期間顯形：換型別的過程中紅燈亮起，唯一要回答的問題是「&lt;strong>換壞了、還是本來就錯&lt;/strong>」。characterization test 的紅燈只有一種含義（行為變了、遷移引入了差異）；如果這批測試混著「找零不該是負數」的正確性期望，紅燈就要逐個歸因——遷移的每一步都拖著這個排查成本。正確性的討論值得做、但排在遷移完成之後、在綠的基線上做：那時改行為的 diff 乾乾淨淨只有行為修正、不會跟型別替換攪在一起。&lt;/p>
&lt;p>這跟&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">測試分診&lt;/a>的洞見同源：測試訊號的價值取決於它的含義夠不夠單一。characterization test 是刻意把含義收窄到「變 / 沒變」一個 bit 的設計。&lt;/p>
&lt;h2 id="退場遷移完成後快照可以轉正">退場：遷移完成後、快照可以轉正&lt;/h2>
&lt;p>characterization test 的生命週期跟著遷移走。遷移全綠收工後有兩條路：把有規格依據的斷言&lt;strong>轉正&lt;/strong>成行為測試（找零的正常路徑）、把當初鎖住的怪癖&lt;strong>開案處理&lt;/strong>（負數歸零是不是該改成拋錯？——現在可以安全地討論了，因為改它的 diff 不會跟遷移混在一起）。留著不動也無害、它繼續當回歸網——但檔名裡的 characterization 字樣要保留，它告訴未來的讀者「這些斷言記錄的是某個時刻的現況、不是設計承諾」。&lt;/p>
&lt;h2 id="前置測試環境的原生依賴三個斷點">前置：測試環境的原生依賴三個斷點&lt;/h2>
&lt;p>這批測試能跑的前提、在同專案 DEVLOG 的除錯史裡有完整的代價記錄——&lt;code>flutter test&lt;/code> 的環境沒有原生 plugin 與 FFI，三個斷點三種症狀：&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;code>LateInitializationError: Field 'packageInfo' ...&lt;/code>&lt;/td>
 &lt;td>&lt;code>package_info_plus&lt;/code> 在測試中不可用、late 欄位沒人初始化&lt;/td>
 &lt;td>&lt;code>TestAppService&lt;/code> 提供 mock &lt;code>PackageInfo&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>storage 相關初始化失敗&lt;/td>
 &lt;td>&lt;code>get_storage&lt;/code> plugin 不存在於測試環境&lt;/td>
 &lt;td>&lt;code>TestStorageProvider&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>Failed to lookup symbol 'init': dlsym(RTLD_DEFAULT, init)&lt;/code>&lt;/td>
 &lt;td>Rive 動畫走原生 FFI、測試環境查無符號&lt;/td>
 &lt;td>&lt;code>MockLoadingPage&lt;/code>——用 &lt;code>Container&lt;/code> + 進度圈保持相同 UI 結構&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三個事件的解法收斂成同一個模式：&lt;strong>原生依賴在測試環境一律要有替身&lt;/strong>、用 &lt;code>Get.put&amp;lt;T&amp;gt;(testInstance)&lt;/code> 綁進依賴注入。症狀查表的價值在診斷速度——&lt;code>dlsym&lt;/code> 字樣指向 FFI、&lt;code>LateInitializationError&lt;/code> 指向沒被初始化的服務欄位、plugin 名字出現在錯誤裡就是 plugin 斷點；三種在第一次遇到時都像靈異事件、記下來之後都是五分鐘的事。Rive 那條的細節值得注意：mock 頁面保持了相同的 UI 結構與 Key，測試驗證邏輯不用改——替身的職責是&lt;strong>補環境、不是簡化被測物&lt;/strong>。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>即將進行「換底不換行為」的遷移（型別、ORM、序列化庫）而現有測試稀疏——先補 characterization、對著舊實作寫、怪癖照鎖&lt;/li>
&lt;li>遷移期間紅燈需要逐個討論「這是不是本來就錯」——正確性期望混進了安全網，拆開&lt;/li>
&lt;li>characterization 檔案在遷移完成很久後仍在、且被當成規格引用——轉正或標註，快照不是承諾&lt;/li>
&lt;li>測試錯誤含 &lt;code>dlsym&lt;/code> / plugin 名 / &lt;code>LateInitializationError&lt;/code>——原生依賴斷點，查替身清單、缺的補上&lt;/li>
&lt;/ul>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>這張安全網守護的遷移：&lt;a href="https://tarrragon.github.io/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 三段遷移&lt;/a>——characterization test 在那篇的角色是配角、本文是它的完整方法&lt;/li>
&lt;li>訊號含義要單一的同源原則：&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">16 個失敗只有 2 個是缺口&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_test_signal_credibility_three_layers/" data-link-title="紅燈在量什麼 — 測試訊號的三層失真：斷言、量測、環境" data-link-desc="「全套件降至 0」有意義的前置條件是紅燈只反映程式缺陷。三層各自會失真：絕對計時斷言量的是機器負載、compact reporter 高並行下行覆寫產生假陰性、fresh checkout 缺 gitignored 生成產物讓整包結果不可信。含 flaky 判定的取樣門檻與對照實驗定歸因的做法。">紅燈在量什麼&lt;/a>&lt;/li>
&lt;li>替身的反面教材：&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_mock_infrastructure_overengineering_deleted/" data-link-title="1101 行自建測試基礎設施、重構刪掉 82.5% — 過度工程的三種形態" data-link-desc="自建測試基礎設施前先問框架的標準做法是什麼：mock 純資料物件、helper 帶並發鎖與記憶體洩漏防護、mock 放進 lib/ 進生產依賴圖，三種形態都在重新發明 Riverpod overrideWith 一行就有的東西。精緻的設計文件不是價值證明——它可以精心規劃一個不需要存在的系統。">1101 行自建測試基礎設施&lt;/a>——替身補環境是正當的、替身變平行框架就過了界&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：POS 專案要做金額型別的全面遷移（Decimal 換 Money extension type），動的是全部 model 的金額欄位。測試目錄裡出現一批名字帶 <code>_characterization_test</code> 的檔案、開頭都有同一段宣告：「在 Money value object 遷移前建立、鎖住現有行為」
<strong>疑問來源</strong>：這批測試跟一般的單元測試差在哪？為什麼連「找零算出負數歸零」這種邊界怪癖也被鎖進去？
<strong>整理目的</strong>：記下 characterization test 的方法、跟正確性測試的分工、以及它依賴的測試環境前置（原生依賴的三個斷點）
<strong>本文邊界</strong>：素材是該專案的五個 characterization 測試檔與 DEVLOG 的測試環境除錯史；characterization test 是 Michael Feathers 在 legacy code 領域的術語、這裡是它在型別遷移的應用</p></blockquote>
<hr>
<h2 id="斷言的對象是現況包括現況的怪癖">斷言的對象是「現況」、包括現況的怪癖</h2>
<p>characterization test 跟一般測試共用全部語法、差別只在斷言的依據：一般測試斷言<strong>規格</strong>（找零應該是多少）、characterization test 斷言<strong>現況</strong>（現在的實作算出多少）。寫法是對著舊實作跑一次、把輸出原樣釘進 expect——實作是測試的 oracle、不是規格。</p>
<p>所以連怪癖也照鎖：現有實作在找零算出負數時歸零、這個行為對不對是另一回事，characterization test 把它鎖住。五個檔案的分佈跟著遷移的影響面走——checkout 的應付金額 fold 與現金找零、order 的小計、cart item 的價格計算、product spec 的三種價格——<strong>遷移會經過的每條金額計算路徑、各有一張行為快照</strong>。</p>
<p>分工的必要性在遷移期間顯形：換型別的過程中紅燈亮起，唯一要回答的問題是「<strong>換壞了、還是本來就錯</strong>」。characterization test 的紅燈只有一種含義（行為變了、遷移引入了差異）；如果這批測試混著「找零不該是負數」的正確性期望，紅燈就要逐個歸因——遷移的每一步都拖著這個排查成本。正確性的討論值得做、但排在遷移完成之後、在綠的基線上做：那時改行為的 diff 乾乾淨淨只有行為修正、不會跟型別替換攪在一起。</p>
<p>這跟<a href="/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">測試分診</a>的洞見同源：測試訊號的價值取決於它的含義夠不夠單一。characterization test 是刻意把含義收窄到「變 / 沒變」一個 bit 的設計。</p>
<h2 id="退場遷移完成後快照可以轉正">退場：遷移完成後、快照可以轉正</h2>
<p>characterization test 的生命週期跟著遷移走。遷移全綠收工後有兩條路：把有規格依據的斷言<strong>轉正</strong>成行為測試（找零的正常路徑）、把當初鎖住的怪癖<strong>開案處理</strong>（負數歸零是不是該改成拋錯？——現在可以安全地討論了，因為改它的 diff 不會跟遷移混在一起）。留著不動也無害、它繼續當回歸網——但檔名裡的 characterization 字樣要保留，它告訴未來的讀者「這些斷言記錄的是某個時刻的現況、不是設計承諾」。</p>
<h2 id="前置測試環境的原生依賴三個斷點">前置：測試環境的原生依賴三個斷點</h2>
<p>這批測試能跑的前提、在同專案 DEVLOG 的除錯史裡有完整的代價記錄——<code>flutter test</code> 的環境沒有原生 plugin 與 FFI，三個斷點三種症狀：</p>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>根因</th>
          <th>替身解法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>LateInitializationError: Field 'packageInfo' ...</code></td>
          <td><code>package_info_plus</code> 在測試中不可用、late 欄位沒人初始化</td>
          <td><code>TestAppService</code> 提供 mock <code>PackageInfo</code></td>
      </tr>
      <tr>
          <td>storage 相關初始化失敗</td>
          <td><code>get_storage</code> plugin 不存在於測試環境</td>
          <td><code>TestStorageProvider</code></td>
      </tr>
      <tr>
          <td><code>Failed to lookup symbol 'init': dlsym(RTLD_DEFAULT, init)</code></td>
          <td>Rive 動畫走原生 FFI、測試環境查無符號</td>
          <td><code>MockLoadingPage</code>——用 <code>Container</code> + 進度圈保持相同 UI 結構</td>
      </tr>
  </tbody>
</table>
<p>三個事件的解法收斂成同一個模式：<strong>原生依賴在測試環境一律要有替身</strong>、用 <code>Get.put&lt;T&gt;(testInstance)</code> 綁進依賴注入。症狀查表的價值在診斷速度——<code>dlsym</code> 字樣指向 FFI、<code>LateInitializationError</code> 指向沒被初始化的服務欄位、plugin 名字出現在錯誤裡就是 plugin 斷點；三種在第一次遇到時都像靈異事件、記下來之後都是五分鐘的事。Rive 那條的細節值得注意：mock 頁面保持了相同的 UI 結構與 Key，測試驗證邏輯不用改——替身的職責是<strong>補環境、不是簡化被測物</strong>。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>即將進行「換底不換行為」的遷移（型別、ORM、序列化庫）而現有測試稀疏——先補 characterization、對著舊實作寫、怪癖照鎖</li>
<li>遷移期間紅燈需要逐個討論「這是不是本來就錯」——正確性期望混進了安全網，拆開</li>
<li>characterization 檔案在遷移完成很久後仍在、且被當成規格引用——轉正或標註，快照不是承諾</li>
<li>測試錯誤含 <code>dlsym</code> / plugin 名 / <code>LateInitializationError</code>——原生依賴斷點，查替身清單、缺的補上</li>
</ul>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>這張安全網守護的遷移：<a href="/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 三段遷移</a>——characterization test 在那篇的角色是配角、本文是它的完整方法</li>
<li>訊號含義要單一的同源原則：<a href="/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">16 個失敗只有 2 個是缺口</a>、<a href="/blog/work-log/flutter_test_signal_credibility_three_layers/" data-link-title="紅燈在量什麼 — 測試訊號的三層失真：斷言、量測、環境" data-link-desc="「全套件降至 0」有意義的前置條件是紅燈只反映程式缺陷。三層各自會失真：絕對計時斷言量的是機器負載、compact reporter 高並行下行覆寫產生假陰性、fresh checkout 缺 gitignored 生成產物讓整包結果不可信。含 flaky 判定的取樣門檻與對照實驗定歸因的做法。">紅燈在量什麼</a></li>
<li>替身的反面教材：<a href="/blog/work-log/flutter_mock_infrastructure_overengineering_deleted/" data-link-title="1101 行自建測試基礎設施、重構刪掉 82.5% — 過度工程的三種形態" data-link-desc="自建測試基礎設施前先問框架的標準做法是什麼：mock 純資料物件、helper 帶並發鎖與記憶體洩漏防護、mock 放進 lib/ 進生產依賴圖，三種形態都在重新發明 Riverpod overrideWith 一行就有的東西。精緻的設計文件不是價值證明——它可以精心規劃一個不需要存在的系統。">1101 行自建測試基礎設施</a>——替身補環境是正當的、替身變平行框架就過了界</li>
</ul>
]]></content:encoded></item></channel></rss>