<?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>Root-Cause on Tarragon</title><link>https://tarrragon.github.io/blog/tags/root-cause/</link><description>Recent content in Root-Cause 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/root-cause/index.xml" rel="self" type="application/rss+xml"/><item><title>Flaky test 根因分類</title><link>https://tarrragon.github.io/blog/testing/05-test-design-judgment/flaky-test-root-cause/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/05-test-design-judgment/flaky-test-root-cause/</guid><description>&lt;p>Flaky test 是指在程式碼沒有改變的情況下，test 的結果在通過和失敗之間隨機切換。Flaky test 侵蝕團隊對 test suite 的信任 — 如果 test 經常「隨便」失敗，開發者會習慣性地 re-run 而非調查失敗原因，真正的 bug 可能在 re-run 中被忽略。&lt;/p>
&lt;h2 id="四類根因">四類根因&lt;/h2>
&lt;h3 id="計時依賴">計時依賴&lt;/h3>
&lt;p>Test 依賴特定的時間條件 — timeout、delay、animation duration。系統負載不同時，時間條件可能滿足也可能不滿足。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>&lt;code>await Future.delayed(Duration(seconds: 2))&lt;/code> + assertion — 如果操作在 2 秒內完成，test 通過；如果 CI 機器負載高導致操作超過 2 秒，test 失敗&lt;/li>
&lt;li>&lt;code>expect(stopwatch.elapsed, lessThan(Duration(seconds: 1)))&lt;/code> — 效能斷言在不同機器上結果不同&lt;/li>
&lt;/ul>
&lt;p>處理策略：用事件驅動代替 timeout。等待 &lt;code>stream.first&lt;/code> 代替 &lt;code>delay(2s) + check&lt;/code>；用 completion signal 代替固定等待時間。如果必須用 timeout，設定寬裕的上限（10x 預期時間）而非精確的預期值。&lt;/p>
&lt;h3 id="環境差異">環境差異&lt;/h3>
&lt;p>Test 在不同環境下行為不同 — 作業系統、檔案系統、時區、locale、DNS 解析。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>檔案路徑分隔符（&lt;code>/&lt;/code> vs &lt;code>\&lt;/code>）在不同 OS 下不同&lt;/li>
&lt;li>時間格式化結果依時區而定（UTC vs local）&lt;/li>
&lt;li>浮點數比較因 CPU 架構不同有微小差異&lt;/li>
&lt;/ul>
&lt;p>處理策略：用 &lt;code>path.join&lt;/code> 代替硬編碼路徑；時間操作用 UTC；浮點比較用 &lt;code>closeTo&lt;/code> 代替精確比較。在 CI 中固定環境變數（&lt;code>TZ=UTC&lt;/code>、&lt;code>LANG=en_US.UTF-8&lt;/code>）。&lt;/p>
&lt;h3 id="資源競爭">資源競爭&lt;/h3>
&lt;p>Test 依賴共享資源（port、暫存檔、資料庫行）— 平行執行時多個 test 同時存取同一資源，結果依賴執行順序。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>多個 test 監聽同一個 port — 第二個綁定失敗&lt;/li>
&lt;li>多個 test 寫入同一個暫存檔 — 內容被覆蓋&lt;/li>
&lt;li>多個 test 操作同一個資料庫 table — 資料互相干擾&lt;/li>
&lt;/ul>
&lt;p>處理策略：每個 test 使用獨立的資源（隨機 port、唯一檔名、隔離的資料庫 schema）。如果資源無法隔離，sequential 執行相關 test（&lt;code>@sequential&lt;/code> 標註）。&lt;/p>
&lt;h3 id="非確定性輸出">非確定性輸出&lt;/h3>
&lt;p>程式碼的輸出本身不確定 — &lt;code>Set&lt;/code> 的迭代順序、&lt;code>Map&lt;/code> 的 key 順序、非同步操作的完成順序。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>斷言 &lt;code>Set&lt;/code> 的 &lt;code>toString()&lt;/code> 結果等於特定字串 — &lt;code>Set&lt;/code> 的迭代順序不保證&lt;/li>
&lt;li>斷言 &lt;code>Future.wait([a, b]).then((results) =&amp;gt; results[0])&lt;/code> — &lt;code>a&lt;/code> 和 &lt;code>b&lt;/code> 的完成順序不固定&lt;/li>
&lt;li>斷言 JSON 序列化的 key 順序 — &lt;code>Map&lt;/code> 的 key 順序在不同實作中不同&lt;/li>
&lt;/ul>
&lt;p>處理策略：不斷言順序（用 &lt;code>containsAll&lt;/code> 代替 &lt;code>equals&lt;/code> 比較集合）；不斷言序列化格式（反序列化後比較值）；用 &lt;code>completion&lt;/code> matcher 代替順序假設。&lt;/p>
&lt;h2 id="診斷步驟">診斷步驟&lt;/h2>
&lt;p>發現疑似 flaky test 時的診斷步驟：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>確認 flaky&lt;/strong>：在乾淨環境連續跑 20 次，確認失敗是隨機的（如果每次都失敗，是 bug 不是 flaky）&lt;/li>
&lt;li>&lt;strong>收集失敗訊息&lt;/strong>：記錄每次失敗的 assertion 訊息、stack trace、環境資訊（OS 版本、CI 機器 ID）&lt;/li>
&lt;li>&lt;strong>分類&lt;/strong>：失敗訊息指向時間（timeout）→ 計時依賴；指向值不同 → 非確定性或環境差異；指向連接失敗 → 資源競爭&lt;/li>
&lt;li>&lt;strong>修復&lt;/strong>：根據分類使用對應的處理策略&lt;/li>
&lt;/ol>
&lt;p>分類和修復之外，flaky test 的根因有時來自 assertion 本身的設計 — &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問&lt;/a>提供判斷 assertion 是否有效的框架。如果 flaky 的根因是 mock 和真實服務的行為差異，回到 &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/mock-boundary-decision/" data-link-title="Mock 邊界判斷決策表" data-link-desc="什麼時候 mock 夠用、什麼時候需要真實服務 — 從 API 層 / 協議層 / 環境層的斷裂點判斷 mock 的適用範圍">Mock 邊界判斷決策表&lt;/a>判斷 mock 是否還適用。Protocol integration test 在 CI 中的&lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/" data-link-title="CI 中的服務 fixture 管理" data-link-desc="在 CI 中啟動和停止真實服務的 test harness 設計 — Process.start / Docker / testcontainers 三種方案的適用場景">服務 fixture 管理&lt;/a>也是 flaky 的常見來源 — 服務啟動不完全就開始跑 test。&lt;/p></description><content:encoded><![CDATA[<p>Flaky test 是指在程式碼沒有改變的情況下，test 的結果在通過和失敗之間隨機切換。Flaky test 侵蝕團隊對 test suite 的信任 — 如果 test 經常「隨便」失敗，開發者會習慣性地 re-run 而非調查失敗原因，真正的 bug 可能在 re-run 中被忽略。</p>
<h2 id="四類根因">四類根因</h2>
<h3 id="計時依賴">計時依賴</h3>
<p>Test 依賴特定的時間條件 — timeout、delay、animation duration。系統負載不同時，時間條件可能滿足也可能不滿足。</p>
<p>常見模式：</p>
<ul>
<li><code>await Future.delayed(Duration(seconds: 2))</code> + assertion — 如果操作在 2 秒內完成，test 通過；如果 CI 機器負載高導致操作超過 2 秒，test 失敗</li>
<li><code>expect(stopwatch.elapsed, lessThan(Duration(seconds: 1)))</code> — 效能斷言在不同機器上結果不同</li>
</ul>
<p>處理策略：用事件驅動代替 timeout。等待 <code>stream.first</code> 代替 <code>delay(2s) + check</code>；用 completion signal 代替固定等待時間。如果必須用 timeout，設定寬裕的上限（10x 預期時間）而非精確的預期值。</p>
<h3 id="環境差異">環境差異</h3>
<p>Test 在不同環境下行為不同 — 作業系統、檔案系統、時區、locale、DNS 解析。</p>
<p>常見模式：</p>
<ul>
<li>檔案路徑分隔符（<code>/</code> vs <code>\</code>）在不同 OS 下不同</li>
<li>時間格式化結果依時區而定（UTC vs local）</li>
<li>浮點數比較因 CPU 架構不同有微小差異</li>
</ul>
<p>處理策略：用 <code>path.join</code> 代替硬編碼路徑；時間操作用 UTC；浮點比較用 <code>closeTo</code> 代替精確比較。在 CI 中固定環境變數（<code>TZ=UTC</code>、<code>LANG=en_US.UTF-8</code>）。</p>
<h3 id="資源競爭">資源競爭</h3>
<p>Test 依賴共享資源（port、暫存檔、資料庫行）— 平行執行時多個 test 同時存取同一資源，結果依賴執行順序。</p>
<p>常見模式：</p>
<ul>
<li>多個 test 監聽同一個 port — 第二個綁定失敗</li>
<li>多個 test 寫入同一個暫存檔 — 內容被覆蓋</li>
<li>多個 test 操作同一個資料庫 table — 資料互相干擾</li>
</ul>
<p>處理策略：每個 test 使用獨立的資源（隨機 port、唯一檔名、隔離的資料庫 schema）。如果資源無法隔離，sequential 執行相關 test（<code>@sequential</code> 標註）。</p>
<h3 id="非確定性輸出">非確定性輸出</h3>
<p>程式碼的輸出本身不確定 — <code>Set</code> 的迭代順序、<code>Map</code> 的 key 順序、非同步操作的完成順序。</p>
<p>常見模式：</p>
<ul>
<li>斷言 <code>Set</code> 的 <code>toString()</code> 結果等於特定字串 — <code>Set</code> 的迭代順序不保證</li>
<li>斷言 <code>Future.wait([a, b]).then((results) =&gt; results[0])</code> — <code>a</code> 和 <code>b</code> 的完成順序不固定</li>
<li>斷言 JSON 序列化的 key 順序 — <code>Map</code> 的 key 順序在不同實作中不同</li>
</ul>
<p>處理策略：不斷言順序（用 <code>containsAll</code> 代替 <code>equals</code> 比較集合）；不斷言序列化格式（反序列化後比較值）；用 <code>completion</code> matcher 代替順序假設。</p>
<h2 id="診斷步驟">診斷步驟</h2>
<p>發現疑似 flaky test 時的診斷步驟：</p>
<ol>
<li><strong>確認 flaky</strong>：在乾淨環境連續跑 20 次，確認失敗是隨機的（如果每次都失敗，是 bug 不是 flaky）</li>
<li><strong>收集失敗訊息</strong>：記錄每次失敗的 assertion 訊息、stack trace、環境資訊（OS 版本、CI 機器 ID）</li>
<li><strong>分類</strong>：失敗訊息指向時間（timeout）→ 計時依賴；指向值不同 → 非確定性或環境差異；指向連接失敗 → 資源競爭</li>
<li><strong>修復</strong>：根據分類使用對應的處理策略</li>
</ol>
<p>分類和修復之外，flaky test 的根因有時來自 assertion 本身的設計 — <a href="/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問</a>提供判斷 assertion 是否有效的框架。如果 flaky 的根因是 mock 和真實服務的行為差異，回到 <a href="/blog/testing/05-test-design-judgment/mock-boundary-decision/" data-link-title="Mock 邊界判斷決策表" data-link-desc="什麼時候 mock 夠用、什麼時候需要真實服務 — 從 API 層 / 協議層 / 環境層的斷裂點判斷 mock 的適用範圍">Mock 邊界判斷決策表</a>判斷 mock 是否還適用。Protocol integration test 在 CI 中的<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 三種方案的適用場景">服務 fixture 管理</a>也是 flaky 的常見來源 — 服務啟動不完全就開始跑 test。</p>
]]></content:encoded></item></channel></rss>