<?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>Http on Tarragon</title><link>https://tarrragon.github.io/blog/tags/http/</link><description>Recent content in Http 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/http/index.xml" rel="self" type="application/rss+xml"/><item><title>模組二：WebSocket 服務架構</title><link>https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> 服務的核心難點是連線建立後的長生命週期管理。HTTP upgrade 只是入口；每個 client 都有讀取、寫入、心跳、訂閱、推送佇列與清理流程。任何一個邊界不清楚，都可能造成 goroutine leak、concurrent write、慢 client 拖垮服務或訂閱狀態不一致。&lt;/p>
&lt;p>本模組承接模組一的並發主題：read pump / write pump 對應 goroutine ownership，heartbeat 對應 select loop 與 ticker，send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>，訂閱集合對應共享狀態與 copy boundary。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/read-write-pump/" data-link-title="2.1 read pump / write pump 模式" data-link-desc="分離 WebSocket 讀取、寫入與心跳">2.1&lt;/a>&lt;/td>
 &lt;td>read pump / write pump 模式&lt;/td>
 &lt;td>讓單一連線的讀取、寫入與清理責任可推理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/heartbeat-deadline/" data-link-title="2.2 heartbeat、deadline 與連線清理" data-link-desc="用 ping/pong 和 deadline 偵測失效連線">2.2&lt;/a>&lt;/td>
 &lt;td>heartbeat、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline&lt;/a> 與連線清理&lt;/td>
 &lt;td>用 ping/pong、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline&lt;/a> 與統一 unregister 偵測失效連線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/subscription-routing/" data-link-title="2.3 訂閱模型與訊息路由" data-link-desc="將 client action 對應到主題訂閱狀態">2.3&lt;/a>&lt;/td>
 &lt;td>訂閱模型與訊息路由&lt;/td>
 &lt;td>把 client action 轉成可測的 command 與訂閱狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">2.4&lt;/a>&lt;/td>
 &lt;td>慢客戶端與 send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 管理&lt;/td>
 &lt;td>用 bounded buffer、drop policy 與 byte budget 控制容量風險&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組使用的範例主題">本模組使用的範例主題&lt;/h2>
&lt;p>本模組使用虛構的即時通知服務作為範例。Client 可以訂閱 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a>，server 會依 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic&lt;/a> 推送 notification、status update 或 error message。&lt;/p>
&lt;p>範例只用來展示 Go &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> 服務設計，不假設讀者正在維護任何特定專案。&lt;/p>
&lt;h2 id="本模組的-go-核心概念">本模組的 Go 核心概念&lt;/h2>
&lt;ul>
&lt;li>用一個 read pump 負責 client 輸入。&lt;/li>
&lt;li>用一個 write pump 負責所有 WebSocket 寫入。&lt;/li>
&lt;li>用 channel 作為 client send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>。&lt;/li>
&lt;li>用 context、done channel 或 hub unregister 管理連線生命週期。&lt;/li>
&lt;li>用 ticker 實作 heartbeat，但由 write pump 統一寫 ping。&lt;/li>
&lt;li>用 mutex 或 hub event loop 保護訂閱狀態。&lt;/li>
&lt;li>用 non-blocking send 保護 hub 不被慢 client 卡住。&lt;/li>
&lt;/ul>
&lt;h2 id="學習重點">學習重點&lt;/h2>
&lt;p>學完本模組後，你應該能判斷：&lt;/p>
&lt;ol>
&lt;li>哪個 goroutine 可以讀 WebSocket，哪個 goroutine 可以寫 WebSocket&lt;/li>
&lt;li>read pump、write pump、hub unregister 之間如何協作&lt;/li>
&lt;li>heartbeat 失敗後應該走哪一條清理路徑&lt;/li>
&lt;li>client action 應該在 router、usecase 還是 hub 裡處理&lt;/li>
&lt;li>send buffer 滿載時應該丟棄、斷線、回錯或改用可靠儲存&lt;/li>
&lt;/ol>
&lt;h2 id="章節粒度說明">章節粒度說明&lt;/h2>
&lt;p>WebSocket 章節依照連線生命週期拆分。Read/write pump、heartbeat、subscription routing、slow client 是四個不同責任；它們常在同一個 hub 或 client type 中互相呼叫，但教學上應分開建立模型。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 服務的核心難點是連線建立後的長生命週期管理。HTTP upgrade 只是入口；每個 client 都有讀取、寫入、心跳、訂閱、推送佇列與清理流程。任何一個邊界不清楚，都可能造成 goroutine leak、concurrent write、慢 client 拖垮服務或訂閱狀態不一致。</p>
<p>本模組承接模組一的並發主題：read pump / write pump 對應 goroutine ownership，heartbeat 對應 select loop 與 ticker，send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 對應 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>，訂閱集合對應共享狀態與 copy boundary。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/go-advanced/02-networking-websocket/read-write-pump/" data-link-title="2.1 read pump / write pump 模式" data-link-desc="分離 WebSocket 讀取、寫入與心跳">2.1</a></td>
          <td>read pump / write pump 模式</td>
          <td>讓單一連線的讀取、寫入與清理責任可推理</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/02-networking-websocket/heartbeat-deadline/" data-link-title="2.2 heartbeat、deadline 與連線清理" data-link-desc="用 ping/pong 和 deadline 偵測失效連線">2.2</a></td>
          <td>heartbeat、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 與連線清理</td>
          <td>用 ping/pong、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 與統一 unregister 偵測失效連線</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/02-networking-websocket/subscription-routing/" data-link-title="2.3 訂閱模型與訊息路由" data-link-desc="將 client action 對應到主題訂閱狀態">2.3</a></td>
          <td>訂閱模型與訊息路由</td>
          <td>把 client action 轉成可測的 command 與訂閱狀態</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">2.4</a></td>
          <td>慢客戶端與 send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 管理</td>
          <td>用 bounded buffer、drop policy 與 byte budget 控制容量風險</td>
      </tr>
  </tbody>
</table>
<h2 id="本模組使用的範例主題">本模組使用的範例主題</h2>
<p>本模組使用虛構的即時通知服務作為範例。Client 可以訂閱 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a>，server 會依 <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 推送 notification、status update 或 error message。</p>
<p>範例只用來展示 Go <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 服務設計，不假設讀者正在維護任何特定專案。</p>
<h2 id="本模組的-go-核心概念">本模組的 Go 核心概念</h2>
<ul>
<li>用一個 read pump 負責 client 輸入。</li>
<li>用一個 write pump 負責所有 WebSocket 寫入。</li>
<li>用 channel 作為 client send <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>。</li>
<li>用 context、done channel 或 hub unregister 管理連線生命週期。</li>
<li>用 ticker 實作 heartbeat，但由 write pump 統一寫 ping。</li>
<li>用 mutex 或 hub event loop 保護訂閱狀態。</li>
<li>用 non-blocking send 保護 hub 不被慢 client 卡住。</li>
</ul>
<h2 id="學習重點">學習重點</h2>
<p>學完本模組後，你應該能判斷：</p>
<ol>
<li>哪個 goroutine 可以讀 WebSocket，哪個 goroutine 可以寫 WebSocket</li>
<li>read pump、write pump、hub unregister 之間如何協作</li>
<li>heartbeat 失敗後應該走哪一條清理路徑</li>
<li>client action 應該在 router、usecase 還是 hub 裡處理</li>
<li>send buffer 滿載時應該丟棄、斷線、回錯或改用可靠儲存</li>
</ol>
<h2 id="章節粒度說明">章節粒度說明</h2>
<p>WebSocket 章節依照連線生命週期拆分。Read/write pump、heartbeat、subscription routing、slow client 是四個不同責任；它們常在同一個 hub 或 client type 中互相呼叫，但教學上應分開建立模型。</p>
<p>如果只想處理單一問題，可以這樣查：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>優先閱讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>concurrent write 或讀寫責任混亂</td>
          <td><a href="/blog/go-advanced/02-networking-websocket/read-write-pump/" data-link-title="2.1 read pump / write pump 模式" data-link-desc="分離 WebSocket 讀取、寫入與心跳">read pump / write pump 模式</a></td>
      </tr>
      <tr>
          <td>連線失效沒有被清理</td>
          <td><a href="/blog/go-advanced/02-networking-websocket/heartbeat-deadline/" data-link-title="2.2 heartbeat、deadline 與連線清理" data-link-desc="用 ping/pong 和 deadline 偵測失效連線">heartbeat、deadline 與連線清理</a></td>
      </tr>
      <tr>
          <td>action、payload、訂閱狀態混在一起</td>
          <td><a href="/blog/go-advanced/02-networking-websocket/subscription-routing/" data-link-title="2.3 訂閱模型與訊息路由" data-link-desc="將 client action 對應到主題訂閱狀態">訂閱模型與訊息路由</a></td>
      </tr>
      <tr>
          <td>慢 client 拖垮 hub</td>
          <td><a href="/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">慢客戶端與 send buffer 管理</a></td>
      </tr>
  </tbody>
</table>
<h2 id="本模組不處理">本模組不處理</h2>
<p>本模組不討論 WebSocket 壓縮、水平擴展、跨節點廣播或完整身份驗證。這些都是實務重要主題，但必須先建立單一 Go process 內的連線生命週期與容量邊界；後續可接 <a href="/blog/go-advanced/07-distributed-operations/cross-node-websocket/" data-link-title="7.3 跨節點 WebSocket、presence 與重連協定" data-link-desc="把單一 server 的 WebSocket hub 擴展到多節點推送與連線狀態">跨節點 WebSocket、presence 與重連協定</a>。</p>
<h2 id="先備知識">先備知識</h2>
<ul>
<li><a href="/blog/go-advanced/01-concurrency-patterns/" data-link-title="模組一：進階並發模式" data-link-desc="channel ownership、select loop、非阻塞送出、共享狀態、worker pool 與 rate limiting">模組一：進階並發模式</a></li>
<li><a href="/blog/go/03-stdlib/http-handler/" data-link-title="3.5 net/http 與 handler 設計" data-link-desc="用 net/http 建立健康檢查、API endpoint 與清楚的 handler 邊界">Go 入門：標準庫 HTTP</a></li>
<li>知道 goroutine、channel、select、context 的基本用法</li>
</ul>
<h2 id="學習時間">學習時間</h2>
<p>預計 3-4 小時</p>
]]></content:encoded></item><item><title>HTTP contract test 設計</title><link>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/http-contract-test/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/http-contract-test/</guid><description>&lt;p>HTTP REST API 的協議複雜度比 WebSocket 低 — request body 是 JSON、response body 是 JSON、status code 有標準語意。但 mock HTTP client（回傳固定 JSON）和真實 API 之間仍然存在差異：error response 的格式、header 的必要性、認證 token 的有效期、rate limit 行為。&lt;/p>
&lt;h2 id="http-protocol-test-的驗證對象">HTTP protocol test 的驗證對象&lt;/h2>
&lt;h3 id="request-格式">Request 格式&lt;/h3>
&lt;p>Client 端發送的 request 是否符合 API 規格。Content-Type header、JSON body 的欄位名稱和型別、query parameter 的格式 — 這些在 mock client 中通常不被驗證（mock 接受任何 request），但真實 API 可能因為格式不符而拒絕。&lt;/p>
&lt;h3 id="response-解析">Response 解析&lt;/h3>
&lt;p>Client 端能否正確解析真實 API 的 response。Mock response 通常是開發者手寫的 JSON，可能和真實 API 的 response 有微妙差異 — 欄位名稱大小寫、數值型別（integer vs float）、null vs 缺失欄位、巢狀結構。&lt;/p>
&lt;h3 id="error-response-處理">Error response 處理&lt;/h3>
&lt;p>真實 API 的 error response 格式可能和 success response 不同。Mock client 通常只模擬 success case，偶爾模擬簡化的 error case。真實 API 的 400/401/403/404/500 各自可能有不同的 error body 結構。&lt;/p>
&lt;h3 id="認證流程">認證流程&lt;/h3>
&lt;p>API 的認證流程（API key、OAuth token、session cookie）在 mock 中通常被跳過。真實 API 的認證包括 token 取得、token 過期、refresh flow — 每一步都可能失敗。&lt;/p>
&lt;h2 id="test-結構">Test 結構&lt;/h2>
&lt;p>HTTP protocol test 的結構和 WebSocket protocol test 類似 — 對真實 API 發送真實 request、驗證真實 response。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">test(&amp;#39;POST /api/resource creates resource&amp;#39;):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> response = await httpClient.post(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &amp;#39;http://localhost:8080/api/resource&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> body: jsonEncode({&amp;#39;name&amp;#39;: &amp;#39;test&amp;#39;, &amp;#39;type&amp;#39;: &amp;#39;A&amp;#39;}),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> headers: {&amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39;, &amp;#39;Authorization&amp;#39;: &amp;#39;Bearer ...&amp;#39;},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> )
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> expect(response.statusCode, 201)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> body = jsonDecode(response.body)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> expect(body[&amp;#39;id&amp;#39;], isNotNull)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> expect(body[&amp;#39;name&amp;#39;], &amp;#39;test&amp;#39;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">test(&amp;#39;POST /api/resource with invalid body returns 400&amp;#39;):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> response = await httpClient.post(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &amp;#39;http://localhost:8080/api/resource&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> body: jsonEncode({&amp;#39;invalid_field&amp;#39;: &amp;#39;value&amp;#39;}),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> headers: {&amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39;, &amp;#39;Authorization&amp;#39;: &amp;#39;Bearer ...&amp;#39;},
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> )
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> expect(response.statusCode, 400)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> body = jsonDecode(response.body)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> expect(body[&amp;#39;error&amp;#39;], isNotNull) // 驗證 error body 結構&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="consumer-driven-contract-test">Consumer-driven contract test&lt;/h2>
&lt;p>當 client 和 server 由不同團隊開發時，consumer-driven contract test 是 protocol integration test 的延伸。Client 團隊定義「我期望的 request/response 格式」（contract），server 團隊驗證 server 實作是否符合 contract。&lt;/p></description><content:encoded><![CDATA[<p>HTTP REST API 的協議複雜度比 WebSocket 低 — request body 是 JSON、response body 是 JSON、status code 有標準語意。但 mock HTTP client（回傳固定 JSON）和真實 API 之間仍然存在差異：error response 的格式、header 的必要性、認證 token 的有效期、rate limit 行為。</p>
<h2 id="http-protocol-test-的驗證對象">HTTP protocol test 的驗證對象</h2>
<h3 id="request-格式">Request 格式</h3>
<p>Client 端發送的 request 是否符合 API 規格。Content-Type header、JSON body 的欄位名稱和型別、query parameter 的格式 — 這些在 mock client 中通常不被驗證（mock 接受任何 request），但真實 API 可能因為格式不符而拒絕。</p>
<h3 id="response-解析">Response 解析</h3>
<p>Client 端能否正確解析真實 API 的 response。Mock response 通常是開發者手寫的 JSON，可能和真實 API 的 response 有微妙差異 — 欄位名稱大小寫、數值型別（integer vs float）、null vs 缺失欄位、巢狀結構。</p>
<h3 id="error-response-處理">Error response 處理</h3>
<p>真實 API 的 error response 格式可能和 success response 不同。Mock client 通常只模擬 success case，偶爾模擬簡化的 error case。真實 API 的 400/401/403/404/500 各自可能有不同的 error body 結構。</p>
<h3 id="認證流程">認證流程</h3>
<p>API 的認證流程（API key、OAuth token、session cookie）在 mock 中通常被跳過。真實 API 的認證包括 token 取得、token 過期、refresh flow — 每一步都可能失敗。</p>
<h2 id="test-結構">Test 結構</h2>
<p>HTTP protocol test 的結構和 WebSocket protocol test 類似 — 對真實 API 發送真實 request、驗證真實 response。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">test(&#39;POST /api/resource creates resource&#39;):
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  response = await httpClient.post(
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    &#39;http://localhost:8080/api/resource&#39;,
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    body: jsonEncode({&#39;name&#39;: &#39;test&#39;, &#39;type&#39;: &#39;A&#39;}),
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    headers: {&#39;Content-Type&#39;: &#39;application/json&#39;, &#39;Authorization&#39;: &#39;Bearer ...&#39;},
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  )
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  expect(response.statusCode, 201)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  body = jsonDecode(response.body)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  expect(body[&#39;id&#39;], isNotNull)
</span></span><span class="line"><span class="ln">10</span><span class="cl">  expect(body[&#39;name&#39;], &#39;test&#39;)
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">test(&#39;POST /api/resource with invalid body returns 400&#39;):
</span></span><span class="line"><span class="ln">13</span><span class="cl">  response = await httpClient.post(
</span></span><span class="line"><span class="ln">14</span><span class="cl">    &#39;http://localhost:8080/api/resource&#39;,
</span></span><span class="line"><span class="ln">15</span><span class="cl">    body: jsonEncode({&#39;invalid_field&#39;: &#39;value&#39;}),
</span></span><span class="line"><span class="ln">16</span><span class="cl">    headers: {&#39;Content-Type&#39;: &#39;application/json&#39;, &#39;Authorization&#39;: &#39;Bearer ...&#39;},
</span></span><span class="line"><span class="ln">17</span><span class="cl">  )
</span></span><span class="line"><span class="ln">18</span><span class="cl">  expect(response.statusCode, 400)
</span></span><span class="line"><span class="ln">19</span><span class="cl">  body = jsonDecode(response.body)
</span></span><span class="line"><span class="ln">20</span><span class="cl">  expect(body[&#39;error&#39;], isNotNull)  // 驗證 error body 結構</span></span></code></pre></div><h2 id="consumer-driven-contract-test">Consumer-driven contract test</h2>
<p>當 client 和 server 由不同團隊開發時，consumer-driven contract test 是 protocol integration test 的延伸。Client 團隊定義「我期望的 request/response 格式」（contract），server 團隊驗證 server 實作是否符合 contract。</p>
<p>Consumer-driven contract test 的工具（Pact、Spring Cloud Contract）自動化了 contract 的定義、驗證和版本管理。適合 API 有多個 consumer 且需要獨立部署的場景。</p>
<p>自用工具或 client/server 同一人開發的場景不需要 contract test 工具 — 直接對真實 server 跑 protocol integration test 更簡單。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>CI 中如何管理 test 用的 server → <a href="/blog/testing/03-protocol-integration-test/service-fixture-management/" data-link-title="CI 中的服務 fixture 管理" data-link-desc="在 CI 中啟動和停止真實服務的 test harness 設計 — Process.start / Docker / testcontainers 三種方案的適用場景">CI 中的服務 fixture 管理</a></li>
<li>WebSocket 的 protocol test → <a href="/blog/testing/03-protocol-integration-test/websocket-protocol-test/" data-link-title="WebSocket 協議測試實作" data-link-desc="對真實 ttyd 驗證 frame type 和 auth handshake — 從 T.C1 和 T.C2 的教訓推導出的 protocol integration test 設計">WebSocket 協議測試實作</a></li>
<li>什麼時候用 contract test 替代 protocol integration test → <a href="/blog/testing/03-protocol-integration-test/cost-judgment/" data-link-title="成本判斷表" data-link-desc="什麼時候值得寫 protocol integration test、什麼時候用 contract test 或實機測試替代 — 根據服務啟動成本和協議複雜度判斷">成本判斷表</a></li>
<li>Backend 的 contract testing 實務 → <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">Backend 可靠性 contract testing</a></li>
</ul>
]]></content:encoded></item></channel></rss>