<?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>Api-Contract on Tarragon</title><link>https://tarrragon.github.io/blog/tags/api-contract/</link><description>Recent content in Api-Contract on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/api-contract/index.xml" rel="self" type="application/rss+xml"/><item><title>Mock 遮蔽機制分析</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/mock-masking-mechanism/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/mock-masking-mechanism/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock 遮蔽" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">Mock 遮蔽&lt;/a>是 mock 的設計邊界。「遮蔽」描述的是機制 — mock 讓協議層差異變得不可見；「盲區」描述的是結果 — 被遮蔽的範圍形成結構性的驗證缺口。Mock 的職責是模擬程式語言層面的 API 契約 — 方法簽名、參數型別、回傳值結構。協議層行為（frame type、handshake 步驟、編碼格式）不在 API 契約的描述範圍內，mock 沒有模擬這些行為的義務，也不應該被期待模擬。&lt;/p>
&lt;h2 id="三層語意與斷裂點">三層語意與斷裂點&lt;/h2>
&lt;p>程式碼和外部服務之間的互動經過三層語意轉換，每一層描述不同粒度的行為。Mock 模擬的是最上層，真實行為發生在下面兩層。&lt;/p>
&lt;h3 id="api-層程式語言的方法簽名">API 層：程式語言的方法簽名&lt;/h3>
&lt;p>API 層描述的是「這個方法接受什麼參數、回傳什麼型別」。Dart 的 &lt;code>WebSocketSink.add&lt;/code> 簽名是 &lt;code>void add(dynamic event)&lt;/code> — 從 API 層看，傳 &lt;code>String&lt;/code> 和傳 &lt;code>Uint8List&lt;/code> 都合法，都不會拋出例外。&lt;/p>
&lt;p>&lt;code>FakeWebSocketChannel&lt;/code> 忠實實作了這個 API 契約。&lt;code>sink.add(&amp;quot;hello&amp;quot;)&lt;/code> 和 &lt;code>sink.add(Uint8List.fromList([104, 101, 108, 108, 111]))&lt;/code> 在 fake 的行為完全相同 — 資料進入內部 buffer，test 可以從 buffer 讀取驗證。Mock 的行為在 API 層是正確的。&lt;/p>
&lt;h3 id="協議層通訊標準的語意規則">協議層：通訊標準的語意規則&lt;/h3>
&lt;p>協議層描述的是「這個資料在網路上如何被編碼、對方如何解讀」。WebSocket 協議（RFC 6455）定義 text frame 用 opcode 0x1、binary frame 用 opcode 0x2 — 兩者語意不同，接收端可以選擇只處理其中一種。&lt;/p>
&lt;p>Dart 的 &lt;code>IOWebSocketChannel&lt;/code>（真實實作）根據 &lt;code>sink.add&lt;/code> 的參數型別決定 frame type：&lt;code>String&lt;/code> 產生 text frame，&lt;code>List&amp;lt;int&amp;gt;&lt;/code> 或 &lt;code>Uint8List&lt;/code> 產生 binary frame。這個行為是 &lt;code>IOWebSocketChannel&lt;/code> 的實作細節，不是 &lt;code>WebSocketSink&lt;/code> 介面契約的一部分 — API 簽名用 &lt;code>dynamic&lt;/code> 把型別資訊抹除了（&lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1&lt;/a>）。&lt;/p>
&lt;p>ttyd 只接受 text frame，收到 binary frame 靜默忽略。從 API 層看，&lt;code>sink.add(Uint8List(...))&lt;/code> 合法；從協議層看，這產生了 ttyd 不處理的 binary frame。斷裂點在 API 層和協議層之間 — mock 模擬了前者，但後者的語意差異只有真實 &lt;code>IOWebSocketChannel&lt;/code> + 真實 ttyd 才會浮現。&lt;/p>
&lt;h3 id="環境層執行環境的行為差異">環境層：執行環境的行為差異&lt;/h3>
&lt;p>環境層描述的是「同一段程式碼在不同執行環境下行為不同」。DNS 解析、TLS 憑證驗證、防火牆規則、作業系統的 socket 實作 — 這些在 test 環境可能和 production 不同。&lt;/p>
&lt;p>環境層的遮蔽比協議層更難處理，因為即使用真實服務做 protocol integration test，test 環境和 production 環境仍可能有差異。本模組不深入環境層議題。&lt;/p>
&lt;h2 id="遮蔽的兩種模式">遮蔽的兩種模式&lt;/h2>
&lt;p>Mock 遮蔽在實務上有兩種不同的表現，需要不同的偵測策略。&lt;/p>
&lt;h3 id="模式一功能存在但行為錯誤">模式一：功能存在但行為錯誤&lt;/h3>
&lt;p>程式碼有對應的實作，但實作的行為和真實服務期望的行為不一致。Mock 讓這個不一致變得不可見，因為 mock 接受了實際上外部服務不會接受的輸入。&lt;/p>
&lt;p>T.C1 就是這種模式。&lt;code>sendData()&lt;/code> 實作了「發送鍵盤輸入」的功能，但發送的是 binary frame 而非 text frame。Mock 的 &lt;code>sink.add(dynamic)&lt;/code> 接受 &lt;code>Uint8List&lt;/code> 不報錯，真實 ttyd 靜默忽略 binary frame。功能存在，行為錯誤，mock 遮蔽了錯誤。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock 遮蔽" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">Mock 遮蔽</a>是 mock 的設計邊界。「遮蔽」描述的是機制 — mock 讓協議層差異變得不可見；「盲區」描述的是結果 — 被遮蔽的範圍形成結構性的驗證缺口。Mock 的職責是模擬程式語言層面的 API 契約 — 方法簽名、參數型別、回傳值結構。協議層行為（frame type、handshake 步驟、編碼格式）不在 API 契約的描述範圍內，mock 沒有模擬這些行為的義務，也不應該被期待模擬。</p>
<h2 id="三層語意與斷裂點">三層語意與斷裂點</h2>
<p>程式碼和外部服務之間的互動經過三層語意轉換，每一層描述不同粒度的行為。Mock 模擬的是最上層，真實行為發生在下面兩層。</p>
<h3 id="api-層程式語言的方法簽名">API 層：程式語言的方法簽名</h3>
<p>API 層描述的是「這個方法接受什麼參數、回傳什麼型別」。Dart 的 <code>WebSocketSink.add</code> 簽名是 <code>void add(dynamic event)</code> — 從 API 層看，傳 <code>String</code> 和傳 <code>Uint8List</code> 都合法，都不會拋出例外。</p>
<p><code>FakeWebSocketChannel</code> 忠實實作了這個 API 契約。<code>sink.add(&quot;hello&quot;)</code> 和 <code>sink.add(Uint8List.fromList([104, 101, 108, 108, 111]))</code> 在 fake 的行為完全相同 — 資料進入內部 buffer，test 可以從 buffer 讀取驗證。Mock 的行為在 API 層是正確的。</p>
<h3 id="協議層通訊標準的語意規則">協議層：通訊標準的語意規則</h3>
<p>協議層描述的是「這個資料在網路上如何被編碼、對方如何解讀」。WebSocket 協議（RFC 6455）定義 text frame 用 opcode 0x1、binary frame 用 opcode 0x2 — 兩者語意不同，接收端可以選擇只處理其中一種。</p>
<p>Dart 的 <code>IOWebSocketChannel</code>（真實實作）根據 <code>sink.add</code> 的參數型別決定 frame type：<code>String</code> 產生 text frame，<code>List&lt;int&gt;</code> 或 <code>Uint8List</code> 產生 binary frame。這個行為是 <code>IOWebSocketChannel</code> 的實作細節，不是 <code>WebSocketSink</code> 介面契約的一部分 — API 簽名用 <code>dynamic</code> 把型別資訊抹除了（<a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1</a>）。</p>
<p>ttyd 只接受 text frame，收到 binary frame 靜默忽略。從 API 層看，<code>sink.add(Uint8List(...))</code> 合法；從協議層看，這產生了 ttyd 不處理的 binary frame。斷裂點在 API 層和協議層之間 — mock 模擬了前者，但後者的語意差異只有真實 <code>IOWebSocketChannel</code> + 真實 ttyd 才會浮現。</p>
<h3 id="環境層執行環境的行為差異">環境層：執行環境的行為差異</h3>
<p>環境層描述的是「同一段程式碼在不同執行環境下行為不同」。DNS 解析、TLS 憑證驗證、防火牆規則、作業系統的 socket 實作 — 這些在 test 環境可能和 production 不同。</p>
<p>環境層的遮蔽比協議層更難處理，因為即使用真實服務做 protocol integration test，test 環境和 production 環境仍可能有差異。本模組不深入環境層議題。</p>
<h2 id="遮蔽的兩種模式">遮蔽的兩種模式</h2>
<p>Mock 遮蔽在實務上有兩種不同的表現，需要不同的偵測策略。</p>
<h3 id="模式一功能存在但行為錯誤">模式一：功能存在但行為錯誤</h3>
<p>程式碼有對應的實作，但實作的行為和真實服務期望的行為不一致。Mock 讓這個不一致變得不可見，因為 mock 接受了實際上外部服務不會接受的輸入。</p>
<p>T.C1 就是這種模式。<code>sendData()</code> 實作了「發送鍵盤輸入」的功能，但發送的是 binary frame 而非 text frame。Mock 的 <code>sink.add(dynamic)</code> 接受 <code>Uint8List</code> 不報錯，真實 ttyd 靜默忽略 binary frame。功能存在，行為錯誤，mock 遮蔽了錯誤。</p>
<p>這種模式的偵測策略是 protocol integration test — 對真實服務發送相同輸入，比對回應是否符合預期。</p>
<h3 id="模式二功能根本沒實作">模式二：功能根本沒實作</h3>
<p>程式碼缺少應有的功能步驟，但 mock 不需要這個步驟就能進入成功狀態。Mock 把多步驟的協議流程簡化成單步操作，讓開發者不知道還有缺少的步驟。</p>
<p>T.C2 就是這種模式。ttyd 要求連線後發送 auth token，但 <code>ConnectionManager</code> 沒有實作這個步驟。<code>FakeWebSocketChannel.ready</code> 立即完成不需認證，<code>stream</code> 由開發者手動控制，不依賴 auth 狀態。Mock 把「TCP 握手 → WS 握手 → auth token → 驗證通過 → 推送資料」這個多步驟流程簡化成「<code>ready</code> 完成 → <code>stream</code> 有資料」（<a href="/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2</a>）。</p>
<p>功能缺失比功能錯誤更難被偵測。功能錯誤至少有一段程式碼可以被 test 覆蓋（只是斷言的對象不夠深）；功能缺失意味著沒有程式碼可以寫 test。只有 protocol integration test 對真實服務跑完整流程，才能暴露「應該有但沒有」的步驟。</p>
<h2 id="mock-不應該模擬協議行為">Mock 不應該模擬協議行為</h2>
<p>面對 mock 遮蔽的第一個直覺反應通常是「讓 mock 更逼真」— 在 <code>FakeWebSocketChannel</code> 裡加入 frame type 區分、auth handshake 驗證等邏輯。這個方向有結構性問題。</p>
<p>Mock 的價值在於簡化 — 把複雜的外部依賴替換成行為可預測的替身，讓 unit test 專注在程式碼邏輯。如果 mock 開始模擬協議行為，mock 本身變成需要維護和驗證的複雜元件。Mock 的正確性由誰保證？如果外部服務更新了協議版本，誰負責更新 mock？</p>
<p>更根本的問題是：即使 mock 完美複製了當前版本的協議行為，它仍然是開發者對協議的理解的副本，不是協議本身。如果開發者對協議的理解就有偏差（例如不知道 ttyd 需要 auth token），mock 會忠實複製這個偏差。</p>
<p>正確的分工是：mock 負責 API 層，protocol integration test 負責協議層。每一層用正確的工具驗證。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>如何辨認偽裝成 integration test 的 mock test → <a href="/blog/testing/01-test-strategy-layers/nominal-integration-test/" data-link-title="「名義 integration test」的識別與修正" data-link-desc="test 名稱含 integration 但核心依賴全用 fake — 如何辨認、為什麼有害、怎麼修正命名和測試策略">名義 integration test 的識別與修正</a></li>
<li>判斷自己的服務是否存在這種斷裂 → <a href="/blog/testing/01-test-strategy-layers/when-protocol-integration-test/" data-link-title="判斷原則：什麼時候需要 protocol integration test" data-link-desc="從服務架構特徵判斷是否需要 protocol integration test 的決策流程 — 協議複雜度、mock 寬鬆度、失敗靜默度三個維度">判斷原則：什麼時候需要 protocol integration test</a></li>
<li>想看 SDK 自動攔截如何影響 mock 遮蔽 → <a href="/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">monitoring 模組三 SDK 設計</a></li>
</ul>
]]></content:encoded></item></channel></rss>