<?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>Testcontainers on Tarragon</title><link>https://tarrragon.github.io/blog/tags/testcontainers/</link><description>Recent content in Testcontainers 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/testcontainers/index.xml" rel="self" type="application/rss+xml"/><item><title>CI 中的服務 fixture 管理</title><link>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/</guid><description>&lt;p>Protocol integration test 需要真實的外部服務實例。在 CI 中管理這些服務實例的啟動、初始化、健康檢查和停止，是 protocol integration test 基礎設施的核心問題。&lt;/p>
&lt;h2 id="三種服務管理方案">三種服務管理方案&lt;/h2>
&lt;h3 id="processstart直接啟動程序">Process.start（直接啟動程序）&lt;/h3>
&lt;p>在 test 的 setUp 中用 &lt;code>Process.start&lt;/code> 啟動服務程序，tearDown 中用 &lt;code>process.kill&lt;/code> 停止。&lt;/p>
&lt;p>適合的前提：服務是單一二進位檔（不需要 Docker），啟動速度快（&amp;lt; 2 秒），不需要持久化狀態。&lt;/p>
&lt;p>app_tunnel 的 ttyd 就是這個模式。&lt;code>ttyd bash&lt;/code> 一行指令啟動，不需要設定檔，不需要資料庫，啟動到可接受連線約 500ms。Test harness 只需要：&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">setUp: process = Process.start(&amp;#39;ttyd&amp;#39;, [&amp;#39;--port&amp;#39;, &amp;#39;7681&amp;#39;, &amp;#39;bash&amp;#39;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> await waitForPort(7681, timeout: 3s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">tearDown: process.kill()&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker-compose">Docker Compose&lt;/h3>
&lt;p>用 Docker Compose 定義服務堆疊，CI 的 before_all 階段 &lt;code>docker compose up&lt;/code>，after_all 階段 &lt;code>docker compose down&lt;/code>。&lt;/p>
&lt;p>適合的前提：服務有依賴（database + cache + app server）、需要特定 OS 環境、需要精確的版本控制。&lt;/p>
&lt;p>Docker Compose 的成本是 image pull 時間（首次或 image 更新時）和容器啟動時間。CI 中可以用 image cache 減少 pull 時間，但冷啟動仍比直接啟動程序慢。&lt;/p>
&lt;h3 id="testcontainers">Testcontainers&lt;/h3>
&lt;p>在 test 程式碼中用 testcontainers 套件管理 Docker 容器。每個 test class 或 test suite 啟動自己的容器，test 結束後自動清理。&lt;/p>
&lt;p>適合的前提：和 Docker Compose 類似，但需要更細粒度的控制（不同 test 用不同的服務設定），或需要在 test 程式碼中動態決定服務的啟動參數。&lt;/p>
&lt;p>Testcontainers 的優勢是 test 和 fixture 在同一個程式碼檔案中，容易理解每個 test 需要什麼環境。缺點是每個 test suite 啟動自己的容器，比共用容器慢。&lt;/p>
&lt;h2 id="健康檢查">健康檢查&lt;/h2>
&lt;p>服務啟動後到可以接受請求之間有延遲。直接在啟動後發送 test request 會因為服務尚未 ready 而失敗。&lt;/p>
&lt;p>健康檢查的方式依服務類型而定：&lt;/p>
&lt;p>&lt;strong>TCP port 可達&lt;/strong>：&lt;code>waitForPort(port, timeout)&lt;/code> 反覆嘗試 TCP 連線，成功即表示服務在監聽。最簡單，適合所有 TCP 服務。&lt;/p>
&lt;p>&lt;strong>HTTP health endpoint&lt;/strong>：對 &lt;code>/health&lt;/code> 或 &lt;code>/ready&lt;/code> 發送 GET request，收到 200 表示服務 ready。比 port check 更可靠 — port 監聽不代表應用層 ready。&lt;/p>
&lt;p>&lt;strong>特定操作成功&lt;/strong>：執行一個輕量的業務操作（例如 WebSocket 連線 + 簡單指令），成功表示服務完全 ready。最可靠但最慢。&lt;/p>
&lt;h2 id="服務狀態隔離">服務狀態隔離&lt;/h2>
&lt;p>不同 test 之間的服務狀態需要隔離 — test A 在服務中建立的資料不應該影響 test B。&lt;/p>
&lt;p>三種隔離策略：&lt;/p>
&lt;p>&lt;strong>每 test 重啟服務&lt;/strong>：最強隔離，最慢。適合服務啟動快（&amp;lt; 1 秒）的場景。&lt;/p>
&lt;p>&lt;strong>每 test 重設狀態&lt;/strong>：服務持續運行，test 開始前清理狀態（truncate tables, flush cache）。適合服務啟動慢但重設快的場景。&lt;/p>
&lt;p>&lt;strong>每 test 用獨立 namespace&lt;/strong>：服務持續運行，每個 test 使用獨立的 database schema / topic / channel。適合支援多租戶的服務。&lt;/p>
&lt;p>app_tunnel 的 ttyd 是無狀態服務（每次連線是獨立的 terminal session），不需要狀態隔離。每個 test 建立新的 WebSocket 連線 = 新的 session。&lt;/p></description><content:encoded><![CDATA[<p>Protocol integration test 需要真實的外部服務實例。在 CI 中管理這些服務實例的啟動、初始化、健康檢查和停止，是 protocol integration test 基礎設施的核心問題。</p>
<h2 id="三種服務管理方案">三種服務管理方案</h2>
<h3 id="processstart直接啟動程序">Process.start（直接啟動程序）</h3>
<p>在 test 的 setUp 中用 <code>Process.start</code> 啟動服務程序，tearDown 中用 <code>process.kill</code> 停止。</p>
<p>適合的前提：服務是單一二進位檔（不需要 Docker），啟動速度快（&lt; 2 秒），不需要持久化狀態。</p>
<p>app_tunnel 的 ttyd 就是這個模式。<code>ttyd bash</code> 一行指令啟動，不需要設定檔，不需要資料庫，啟動到可接受連線約 500ms。Test harness 只需要：</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">setUp: process = Process.start(&#39;ttyd&#39;, [&#39;--port&#39;, &#39;7681&#39;, &#39;bash&#39;])
</span></span><span class="line"><span class="ln">2</span><span class="cl">       await waitForPort(7681, timeout: 3s)
</span></span><span class="line"><span class="ln">3</span><span class="cl">tearDown: process.kill()</span></span></code></pre></div><h3 id="docker-compose">Docker Compose</h3>
<p>用 Docker Compose 定義服務堆疊，CI 的 before_all 階段 <code>docker compose up</code>，after_all 階段 <code>docker compose down</code>。</p>
<p>適合的前提：服務有依賴（database + cache + app server）、需要特定 OS 環境、需要精確的版本控制。</p>
<p>Docker Compose 的成本是 image pull 時間（首次或 image 更新時）和容器啟動時間。CI 中可以用 image cache 減少 pull 時間，但冷啟動仍比直接啟動程序慢。</p>
<h3 id="testcontainers">Testcontainers</h3>
<p>在 test 程式碼中用 testcontainers 套件管理 Docker 容器。每個 test class 或 test suite 啟動自己的容器，test 結束後自動清理。</p>
<p>適合的前提：和 Docker Compose 類似，但需要更細粒度的控制（不同 test 用不同的服務設定），或需要在 test 程式碼中動態決定服務的啟動參數。</p>
<p>Testcontainers 的優勢是 test 和 fixture 在同一個程式碼檔案中，容易理解每個 test 需要什麼環境。缺點是每個 test suite 啟動自己的容器，比共用容器慢。</p>
<h2 id="健康檢查">健康檢查</h2>
<p>服務啟動後到可以接受請求之間有延遲。直接在啟動後發送 test request 會因為服務尚未 ready 而失敗。</p>
<p>健康檢查的方式依服務類型而定：</p>
<p><strong>TCP port 可達</strong>：<code>waitForPort(port, timeout)</code> 反覆嘗試 TCP 連線，成功即表示服務在監聽。最簡單，適合所有 TCP 服務。</p>
<p><strong>HTTP health endpoint</strong>：對 <code>/health</code> 或 <code>/ready</code> 發送 GET request，收到 200 表示服務 ready。比 port check 更可靠 — port 監聽不代表應用層 ready。</p>
<p><strong>特定操作成功</strong>：執行一個輕量的業務操作（例如 WebSocket 連線 + 簡單指令），成功表示服務完全 ready。最可靠但最慢。</p>
<h2 id="服務狀態隔離">服務狀態隔離</h2>
<p>不同 test 之間的服務狀態需要隔離 — test A 在服務中建立的資料不應該影響 test B。</p>
<p>三種隔離策略：</p>
<p><strong>每 test 重啟服務</strong>：最強隔離，最慢。適合服務啟動快（&lt; 1 秒）的場景。</p>
<p><strong>每 test 重設狀態</strong>：服務持續運行，test 開始前清理狀態（truncate tables, flush cache）。適合服務啟動慢但重設快的場景。</p>
<p><strong>每 test 用獨立 namespace</strong>：服務持續運行，每個 test 使用獨立的 database schema / topic / channel。適合支援多租戶的服務。</p>
<p>app_tunnel 的 ttyd 是無狀態服務（每次連線是獨立的 terminal session），不需要狀態隔離。每個 test 建立新的 WebSocket 連線 = 新的 session。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>什麼時候值得建 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>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>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>
</ul>
]]></content:encoded></item></channel></rss>