<?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>除錯 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E9%99%A4%E9%8C%AF/</link><description>Recent content in 除錯 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 12 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E9%99%A4%E9%8C%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>測試全過但有 Bug</title><link>https://tarrragon.github.io/blog/record/%E6%B8%AC%E8%A9%A6%E5%85%A8%E9%81%8E%E4%BD%86%E6%9C%89-bug/</link><pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E6%B8%AC%E8%A9%A6%E5%85%A8%E9%81%8E%E4%BD%86%E6%9C%89-bug/</guid><description>&lt;blockquote>
&lt;p>2026-03，開發線上點單多廚房印表機列印功能。
寫了 28 個測試，全部通過，上實機後陸續發現四個 Bug。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="發生了什麼事">發生了什麼事&lt;/h2>
&lt;p>功能的核心流程：&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">收到追加點單
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → 把品項分派到對應的廚房印表機
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → 組裝收據內容（標題、品名+數量、備註等）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> → 逐行列印
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> → 單行文字（標題、桌號）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> → 多欄表格（品名 + 數量）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>上實機後，四個 Bug 依序出現——因為它們在同一條執行路徑的不同深度，每修一個，程式才能走到下一個：&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">Bug 1（印表機內部元件未初始化）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ 修好，程式碼走得更遠
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Bug 2（品項分派邏輯錯誤，全部送到同一台）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ 修好，兩台印表機都有被呼叫
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">Bug 3（多欄表格的欄位寬度不符合規定）
&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">Bug 4（空行觸發第三方 library 的越界錯誤）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Bug&lt;/th>
 &lt;th>出了什麼事&lt;/th>
 &lt;th>測試沒抓到的原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Bug 1: 印表機元件未初始化&lt;/td>
 &lt;td>模擬印表機的初始化漏掉了內部元件&lt;/td>
 &lt;td>只測了「送出資料」，沒測「組裝列印指令」這個步驟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bug 2: 品項全分到同一台&lt;/td>
 &lt;td>分派邏輯找到第一台可用的印表機就全部送過去&lt;/td>
 &lt;td>手動構造預期結果，分派邏輯沒有被執行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bug 3: 欄位寬度錯誤&lt;/td>
 &lt;td>欄位比例（3:1=4）不符合 library 要求的總和 12&lt;/td>
 &lt;td>模擬的收據內容只有純文字行，沒有多欄表格行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bug 4: 空行越界錯誤&lt;/td>
 &lt;td>第三方 library 沒有處理空字串&lt;/td>
 &lt;td>被 Bug 3 擋住，程式從未執行到這一行&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="為什麼一次只能發現一個-bug">為什麼一次只能發現一個 Bug&lt;/h2>
&lt;p>四個 Bug 都在同一條執行路徑上，只是深度不同。程式走到第一個錯誤就中斷了，後面的都被遮蔽。&lt;/p>
&lt;p>而測試沒有發現這些問題，是因為測試沒有走過這條完整路徑。&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">真實路徑： 接收訂單 → 組裝收據 → 列印中心 → 表格列印/文字列印 → 印表機底層
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">測試路徑： 接收訂單 → 中斷（手動構造結果，後面都沒跑）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">後來補了模擬元件：接收訂單 → 模擬收據產生器 → 列印中心 → 模擬印表機 → 底層
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> ↑
&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"> → 文字列印有被覆蓋，表格列印沒有
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> → Bug 3, 4 仍然隱藏&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這次的問題在測試覆蓋的路徑深度。&lt;/p>
&lt;hr>
&lt;h2 id="回顧這次遇到的五個事故">回顧：這次遇到的五個事故&lt;/h2>
&lt;h3 id="1-測試的是手動構造的結果不是程式的行為">1. 測試的是「手動構造的結果」，不是「程式的行為」&lt;/h3>
&lt;p>&lt;strong>怎麼發現的：&lt;/strong> 實機上兩台廚房印表機只有一台收到品項，另一台完全沒動。但測試裡「品項分派邏輯」的測試案例是通過的。&lt;/p>
&lt;p>&lt;strong>怎麼找到原因的：&lt;/strong> 回頭看測試程式碼，發現測試裡的分派結果是手動寫死的，不是由分派邏輯算出來的：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;品名長度奇數分配到第一台&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OnlineOrderPrintResult&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nl">itemPrinterMapping:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;item-1&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;kitchen-2&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span> &lt;span class="c1">// 寫死的值
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="n">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">applyPrintResult&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kitchenItemPrintJobs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;item-1&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">printerId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;kitchen-2&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個測試驗證的是「把結果存進去再讀出來，資料有沒有一致」，但品項分派的程式碼從頭到尾沒有被執行過。測試名稱寫的是分派邏輯，實際測的是資料儲存。&lt;/p>
&lt;p>&lt;strong>怎麼修的：&lt;/strong> 改成從入口方法開始呼叫，讓品項分派的邏輯實際跑一遍。跑完之後 Bug 2 就出現了——分派邏輯的 fallback 條件寫錯，所有品項都被送到同一台。&lt;/p>
&lt;hr>
&lt;h3 id="2-只測了子類別自己的方法沒測從父類別繼承的方法">2. 只測了子類別自己的方法，沒測從父類別繼承的方法&lt;/h3>
&lt;p>&lt;strong>怎麼發現的：&lt;/strong> 實機上廚房印表機列印全部失敗，log 顯示內部元件未初始化的錯誤，但測試裡模擬印表機的初始化和列印測試都是通過的。&lt;/p>
&lt;p>&lt;strong>怎麼找到原因的：&lt;/strong> 比對測試和實際程式碼的呼叫路徑。測試裡呼叫的是模擬印表機自己覆寫的「送出資料」方法（改成什麼都不做），但實際列印時上層呼叫的是從父類別繼承的「組裝列印指令」方法，這個方法內部依賴一個需要初始化的元件。測試覆蓋到的方法，和實際執行路徑走到的方法不是同一個。&lt;/p>
&lt;p>&lt;strong>怎麼修的：&lt;/strong> 在測試中加入對繼承方法的測試——初始化後呼叫「組裝列印指令」確認不報錯，以及未初始化時呼叫確認會拋出錯誤。同時修正模擬印表機的初始化方法，補上內部元件的建立。&lt;/p>
&lt;hr>
&lt;h3 id="3-斷言只檢查有沒有沒檢查對不對">3. 斷言只檢查「有沒有」，沒檢查「對不對」&lt;/h3>
&lt;p>&lt;strong>怎麼發現的：&lt;/strong> 修完 Bug 1 和 2 之後重跑測試，整合測試通過了。但看 log 發現廚房 1 和廚房 2 的列印結果都是 &lt;code>false&lt;/code>（失敗），和測試通過的結果矛盾。&lt;/p>
&lt;p>&lt;strong>怎麼找到原因的：&lt;/strong> 回頭看斷言，發現寫的是 &lt;code>containsKey&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kitchenResults&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">containsKey&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;kitchen-1&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">isTrue&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個斷言只檢查「有沒有這台印表機的結果」，不管結果是成功還是失敗。列印在 try-catch 裡失敗後回傳 &lt;code>false&lt;/code>，但 key 存在，所以斷言通過。&lt;/p>
&lt;p>&lt;strong>怎麼修的：&lt;/strong> 改成直接檢查值 &lt;code>expect(result.kitchenResults['kitchen-1'], isTrue)&lt;/code>。改完之後測試立刻失敗，顯示列印結果確實是 &lt;code>false&lt;/code>。這才發現測試環境缺少收據產生器的依賴，列印路徑在組裝收據的步驟就斷了，被 try-catch 吞掉回傳 &lt;code>false&lt;/code>。&lt;/p>
&lt;hr>
&lt;h3 id="4-模擬元件的回傳資料只覆蓋了部分分支">4. 模擬元件的回傳資料只覆蓋了部分分支&lt;/h3>
&lt;p>&lt;strong>怎麼發現的：&lt;/strong> 修完 Bug 1、2，也修正了斷言（坑 3）之後，測試全過，列印結果也都是 &lt;code>true&lt;/code>。但上實機測試時，廚房印表機仍然全部失敗，log 顯示「欄位寬度總和必須等於 12」。&lt;/p>
&lt;p>&lt;strong>怎麼找到原因的：&lt;/strong> 測試環境和實機的差異在於收據的內容。實機用的是真實的廚房收據模板，包含多欄表格（品名+數量）。測試用的是模擬的收據產生器，只回傳一行純文字：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">FakeReceiptBuilderService&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">ReceiptBuilderService&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">Future&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">ReceiptLine&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">buildReceiptLines&lt;/span>&lt;span class="p">(...)&lt;/span> &lt;span class="kd">async&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">ReceiptLine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">singleLine&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">)];&lt;/span> &lt;span class="c1">// 只有標題
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>純文字走的是「文字列印」，多欄表格走的是「表格列印」——這是兩條不同的分支。模擬的資料只觸發了文字列印，表格列印從未被測試執行過。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>2026-03，開發線上點單多廚房印表機列印功能。
寫了 28 個測試，全部通過，上實機後陸續發現四個 Bug。</p></blockquote>
<hr>
<h2 id="發生了什麼事">發生了什麼事</h2>
<p>功能的核心流程：</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">收到追加點單
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → 把品項分派到對應的廚房印表機
</span></span><span class="line"><span class="ln">3</span><span class="cl">  → 組裝收據內容（標題、品名+數量、備註等）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  → 逐行列印
</span></span><span class="line"><span class="ln">5</span><span class="cl">    → 單行文字（標題、桌號）
</span></span><span class="line"><span class="ln">6</span><span class="cl">    → 多欄表格（品名 + 數量）</span></span></code></pre></div><p>上實機後，四個 Bug 依序出現——因為它們在同一條執行路徑的不同深度，每修一個，程式才能走到下一個：</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">Bug 1（印表機內部元件未初始化）
</span></span><span class="line"><span class="ln">2</span><span class="cl">  ↓ 修好，程式碼走得更遠
</span></span><span class="line"><span class="ln">3</span><span class="cl">Bug 2（品項分派邏輯錯誤，全部送到同一台）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  ↓ 修好，兩台印表機都有被呼叫
</span></span><span class="line"><span class="ln">5</span><span class="cl">Bug 3（多欄表格的欄位寬度不符合規定）
</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">Bug 4（空行觸發第三方 library 的越界錯誤）</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>Bug</th>
          <th>出了什麼事</th>
          <th>測試沒抓到的原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bug 1: 印表機元件未初始化</td>
          <td>模擬印表機的初始化漏掉了內部元件</td>
          <td>只測了「送出資料」，沒測「組裝列印指令」這個步驟</td>
      </tr>
      <tr>
          <td>Bug 2: 品項全分到同一台</td>
          <td>分派邏輯找到第一台可用的印表機就全部送過去</td>
          <td>手動構造預期結果，分派邏輯沒有被執行</td>
      </tr>
      <tr>
          <td>Bug 3: 欄位寬度錯誤</td>
          <td>欄位比例（3:1=4）不符合 library 要求的總和 12</td>
          <td>模擬的收據內容只有純文字行，沒有多欄表格行</td>
      </tr>
      <tr>
          <td>Bug 4: 空行越界錯誤</td>
          <td>第三方 library 沒有處理空字串</td>
          <td>被 Bug 3 擋住，程式從未執行到這一行</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="為什麼一次只能發現一個-bug">為什麼一次只能發現一個 Bug</h2>
<p>四個 Bug 都在同一條執行路徑上，只是深度不同。程式走到第一個錯誤就中斷了，後面的都被遮蔽。</p>
<p>而測試沒有發現這些問題，是因為測試沒有走過這條完整路徑。</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">真實路徑：  接收訂單 → 組裝收據 → 列印中心 → 表格列印/文字列印 → 印表機底層
</span></span><span class="line"><span class="ln">2</span><span class="cl">測試路徑：  接收訂單 → 中斷（手動構造結果，後面都沒跑）
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">後來補了模擬元件：接收訂單 → 模擬收據產生器 → 列印中心 → 模擬印表機 → 底層
</span></span><span class="line"><span class="ln">5</span><span class="cl">                                  ↑
</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">                           → 文字列印有被覆蓋，表格列印沒有
</span></span><span class="line"><span class="ln">8</span><span class="cl">                           → Bug 3, 4 仍然隱藏</span></span></code></pre></div><p>這次的問題在測試覆蓋的路徑深度。</p>
<hr>
<h2 id="回顧這次遇到的五個事故">回顧：這次遇到的五個事故</h2>
<h3 id="1-測試的是手動構造的結果不是程式的行為">1. 測試的是「手動構造的結果」，不是「程式的行為」</h3>
<p><strong>怎麼發現的：</strong> 實機上兩台廚房印表機只有一台收到品項，另一台完全沒動。但測試裡「品項分派邏輯」的測試案例是通過的。</p>
<p><strong>怎麼找到原因的：</strong> 回頭看測試程式碼，發現測試裡的分派結果是手動寫死的，不是由分派邏輯算出來的：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;品名長度奇數分配到第一台&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="n">OnlineOrderPrintResult</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nl">itemPrinterMapping:</span> <span class="p">{</span><span class="s1">&#39;item-1&#39;</span><span class="o">:</span> <span class="s1">&#39;kitchen-2&#39;</span><span class="p">},</span> <span class="c1">// 寫死的值
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="n">record</span><span class="p">.</span><span class="n">applyPrintResult</span><span class="p">(</span><span class="n">result</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">record</span><span class="p">.</span><span class="n">kitchenItemPrintJobs</span><span class="p">[</span><span class="s1">&#39;item-1&#39;</span><span class="p">]</span><span class="o">!</span><span class="p">.</span><span class="n">printerId</span><span class="p">,</span> <span class="s1">&#39;kitchen-2&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>這個測試驗證的是「把結果存進去再讀出來，資料有沒有一致」，但品項分派的程式碼從頭到尾沒有被執行過。測試名稱寫的是分派邏輯，實際測的是資料儲存。</p>
<p><strong>怎麼修的：</strong> 改成從入口方法開始呼叫，讓品項分派的邏輯實際跑一遍。跑完之後 Bug 2 就出現了——分派邏輯的 fallback 條件寫錯，所有品項都被送到同一台。</p>
<hr>
<h3 id="2-只測了子類別自己的方法沒測從父類別繼承的方法">2. 只測了子類別自己的方法，沒測從父類別繼承的方法</h3>
<p><strong>怎麼發現的：</strong> 實機上廚房印表機列印全部失敗，log 顯示內部元件未初始化的錯誤，但測試裡模擬印表機的初始化和列印測試都是通過的。</p>
<p><strong>怎麼找到原因的：</strong> 比對測試和實際程式碼的呼叫路徑。測試裡呼叫的是模擬印表機自己覆寫的「送出資料」方法（改成什麼都不做），但實際列印時上層呼叫的是從父類別繼承的「組裝列印指令」方法，這個方法內部依賴一個需要初始化的元件。測試覆蓋到的方法，和實際執行路徑走到的方法不是同一個。</p>
<p><strong>怎麼修的：</strong> 在測試中加入對繼承方法的測試——初始化後呼叫「組裝列印指令」確認不報錯，以及未初始化時呼叫確認會拋出錯誤。同時修正模擬印表機的初始化方法，補上內部元件的建立。</p>
<hr>
<h3 id="3-斷言只檢查有沒有沒檢查對不對">3. 斷言只檢查「有沒有」，沒檢查「對不對」</h3>
<p><strong>怎麼發現的：</strong> 修完 Bug 1 和 2 之後重跑測試，整合測試通過了。但看 log 發現廚房 1 和廚房 2 的列印結果都是 <code>false</code>（失敗），和測試通過的結果矛盾。</p>
<p><strong>怎麼找到原因的：</strong> 回頭看斷言，發現寫的是 <code>containsKey</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">kitchenResults</span><span class="p">.</span><span class="n">containsKey</span><span class="p">(</span><span class="s1">&#39;kitchen-1&#39;</span><span class="p">),</span> <span class="n">isTrue</span><span class="p">);</span></span></span></code></pre></div><p>這個斷言只檢查「有沒有這台印表機的結果」，不管結果是成功還是失敗。列印在 try-catch 裡失敗後回傳 <code>false</code>，但 key 存在，所以斷言通過。</p>
<p><strong>怎麼修的：</strong> 改成直接檢查值 <code>expect(result.kitchenResults['kitchen-1'], isTrue)</code>。改完之後測試立刻失敗，顯示列印結果確實是 <code>false</code>。這才發現測試環境缺少收據產生器的依賴，列印路徑在組裝收據的步驟就斷了，被 try-catch 吞掉回傳 <code>false</code>。</p>
<hr>
<h3 id="4-模擬元件的回傳資料只覆蓋了部分分支">4. 模擬元件的回傳資料只覆蓋了部分分支</h3>
<p><strong>怎麼發現的：</strong> 修完 Bug 1、2，也修正了斷言（坑 3）之後，測試全過，列印結果也都是 <code>true</code>。但上實機測試時，廚房印表機仍然全部失敗，log 顯示「欄位寬度總和必須等於 12」。</p>
<p><strong>怎麼找到原因的：</strong> 測試環境和實機的差異在於收據的內容。實機用的是真實的廚房收據模板，包含多欄表格（品名+數量）。測試用的是模擬的收據產生器，只回傳一行純文字：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">class</span> <span class="nc">FakeReceiptBuilderService</span> <span class="kd">extends</span> <span class="n">ReceiptBuilderService</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">Future</span><span class="o">&lt;</span><span class="n">List</span><span class="o">&lt;</span><span class="n">ReceiptLine</span><span class="o">&gt;&gt;</span> <span class="n">buildReceiptLines</span><span class="p">(...)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">ReceiptLine</span><span class="p">.</span><span class="n">singleLine</span><span class="p">(</span><span class="n">data</span><span class="p">.</span><span class="n">title</span><span class="p">)];</span> <span class="c1">// 只有標題
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>純文字走的是「文字列印」，多欄表格走的是「表格列印」——這是兩條不同的分支。模擬的資料只觸發了文字列印，表格列印從未被測試執行過。</p>
<ul>
<li>純文字列印有被覆蓋 → 沒問題</li>
<li>多欄表格列印沒有被觸發 → Bug 3 仍然隱藏</li>
<li>空行列印沒有被觸發 → Bug 4 仍然隱藏</li>
</ul>
<p><strong>怎麼修的：</strong> 在印表機的表格列印方法中加入自動正規化，將欄位比例換算為符合 library 要求的總和 12。這是適配層的修復，所有收據模板都不需要修改。</p>
<hr>
<h3 id="5-第三方-library-的地雷前面都做對了才踩到">5. 第三方 library 的地雷——前面都做對了才踩到</h3>
<p><strong>怎麼發現的：</strong> 修完 Bug 3 之後再上實機，廚房印表機仍然失敗，但錯誤訊息不同了——從「欄位寬度總和必須等於 12」變成「RangeError: Valid value range is empty: 0」。</p>
<p><strong>怎麼找到原因的：</strong> 從 log 看到列印標題和桌號成功（兩次資料送出），在第三行（空行）就失敗了。追蹤到第三方 library 的原始碼，發現它在解析文字時會取第一個字元來判斷是否為中文字，但沒有處理空字串的情況，直接對空字串取 <code>text[0]</code> 導致越界。</p>
<p>這個問題一直存在，但之前 Bug 3 擋在前面（程式在表格列印就失敗了，走不到後面的空行列印），前三個 Bug 都修好之後，執行路徑才真正打通，觸發了這個潛在問題。</p>
<p>從另一個角度看，能走到 Bug 4 代表前面的修復都是有效的。</p>
<p><strong>怎麼修的：</strong> 在呼叫 library 之前加了空字串的前置檢查，遇到空字串時改用換行指令代替，繞過 library 的問題。</p>
<hr>
<h3 id="6-try-catch-的範圍太大把程式碼-bug-和硬體故障混在一起處理">6. try-catch 的範圍太大，把程式碼 bug 和硬體故障混在一起處理</h3>
<p><strong>怎麼發現的：</strong> 回顧整個除錯過程，Bug 1、3、4 都有一個共同特徵——錯誤被 try-catch 吞掉，回傳 <code>false</code>，沒有任何明顯的異常。在測試中，缺少依賴的情況也被同樣的 try-catch 吞掉，測試照樣通過。</p>
<p><strong>怎麼找到原因的：</strong> 看列印方法的 catch 區塊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">Future</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;</span> <span class="n">_printKitchenReceipt</span><span class="p">(...)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1">// 組裝收據資料
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>    <span class="c1">// 組裝收據行
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="c1">// 呼叫印表機列印
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">debugPrint</span><span class="p">(</span><span class="s1">&#39;failed: </span><span class="si">$</span><span class="n">e</span><span class="s1">&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>catch (e)</code> 攔截了所有錯誤，不區分類型。但這裡面混了兩種性質不同的錯誤：</p>
<ul>
<li><strong>印表機故障</strong>（連線逾時、無紙、裝置離線）→ 執行期的預期狀況，應該攔截，回傳失敗讓 UI 顯示重印按鈕</li>
<li><strong>程式碼 bug</strong>（未初始化的元件、欄位寬度不合法、空字串越界）→ 開發階段就該被發現的問題，不應該被靜默吞掉</li>
</ul>
<p>四個 Bug 裡有三個屬於後者，全部被同一個 <code>catch (e)</code> 攔住，在開發和測試階段都沒有任何異常跡象。</p>
<p><strong>怎麼修的：</strong> 做了三件事：</p>
<ol>
<li>定義 <code>PrinterException</code>，專門代表印表機硬體/連線錯誤</li>
<li>在列印中心（IO 邊界）把印表機拋出的 <code>Exception</code> 包成 <code>PrinterException</code>，但不攔截 <code>Error</code>（程式碼 bug）</li>
<li>列印方法改為 <code>on PrinterException catch</code>，只處理印表機故障</li>
</ol>
<p>改動前後的對比：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 改動前：所有錯誤都被吞掉
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="n">lines</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">receiptBuilder</span><span class="p">.</span><span class="n">buildReceiptLines</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">template</span><span class="p">);</span>  <span class="c1">// ← 出錯也被吞
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kd">await</span> <span class="n">printCenter</span><span class="p">.</span><span class="n">printReceiptLines</span><span class="p">(</span><span class="nl">lines:</span> <span class="n">lines</span><span class="p">,</span> <span class="nl">printer:</span> <span class="n">printer</span><span class="p">);</span>   <span class="c1">// ← 出錯也被吞
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 改動後：資料準備不在 try 裡，只攔截印表機錯誤
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">lines</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">receiptBuilder</span><span class="p">.</span><span class="n">buildReceiptLines</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">template</span><span class="p">);</span>  <span class="c1">// ← 出錯直接拋出
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="kd">await</span> <span class="n">printCenter</span><span class="p">.</span><span class="n">printReceiptLines</span><span class="p">(</span><span class="nl">lines:</span> <span class="n">lines</span><span class="p">,</span> <span class="nl">printer:</span> <span class="n">printer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span> <span class="n">on</span> <span class="n">PrinterException</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// ← 只攔截印表機故障
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>改完之後，測試裡缺少依賴的情況不再被吞掉——之前有一組測試預期列印結果是 <code>false</code>（因為缺收據產生器被 catch 吞掉），現在補上依賴後預期改為 <code>true</code>，測試驗證的是真正的列印行為。</p>
<hr>
<h2 id="從這次經驗歸納的測試方法">從這次經驗歸納的測試方法</h2>
<h3 id="一從呼叫路徑出發而非從程式碼結構出發">一、從呼叫路徑出發，而非從程式碼結構出發</h3>
<p>這次犯的最大錯誤是按照「這個 class 有哪些方法」來分配測試，結果每個方法各自通過，但串在一起就出問題。後來改成按「使用者操作觸發了什麼路徑」來規劃：</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">使用者操作                    要測試的完整路徑
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">─────────                    ──────────────
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">追加點餐送出     →  handler.printAppendedOrder
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                      → _buildItemPrinterMapping  ← 分派邏輯
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                      → buildReceiptLines          ← 收據組裝
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                      → printReceiptLines           ← 實際列印
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                      → printText / printRow        ← 印表機操作
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">點擊重印按鈕     →  retryItemKitchenPrint
</span></span><span class="line"><span class="ln">10</span><span class="cl">                      → printAppendedOrder(kitchenItemIds: {itemId})</span></span></code></pre></div><p>按路徑規劃之後，每個測試案例都會走過完整的鏈路，中間環節的問題自然會被觸發。</p>
<h3 id="二整合測試與單元測試的分工">二、整合測試與單元測試的分工</h3>
<p>這次的六個坑裡，有四個（坑 1、3、4、6）屬於「元件之間銜接」的問題，單元測試各自通過但串接失敗。回頭看分工：</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">                    單元測試                     整合測試
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">─────────────────────────────────────────────────────────
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">測什麼？          單一方法的輸入輸出             多個元件串接的結果
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">假設什麼？        其他元件是正確的               驗證元件之間的銜接
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">能抓到什麼 Bug？  演算法邏輯錯誤                 初始化遺漏、依賴缺失、
</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">本案例中          KitchenPrinterConfig           printAppendedOrder +
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                  .handlesProduct                PrintCenter + FakePrinter +
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                  → 匹配邏輯正確                 ReceiptBuilderService
</span></span><span class="line"><span class="ln">10</span><span class="cl">                                                → 端到端路徑正確</span></span></code></pre></div><p>這次的經驗是：功能涉及多個元件協作時，只有單元測試是不夠的。整合測試才能抓到元件之間的銜接問題。</p>
<h3 id="三替-try-catch-設計專門的測試">三、替 try-catch 設計專門的測試</h3>
<p>try-catch 在這次經驗裡反覆出現——Bug 1、3、4 被它吞掉，坑 3 的斷言因為它而失效，坑 6 則是根本性的設計問題。</p>
<p>回顧後歸納的三個對策：</p>
<ul>
<li><strong>斷言成功路徑的值</strong>：不只檢查「沒拋錯」，要檢查回傳值是 <code>true</code>。坑 3 就是因為只檢查 key 存在，沒檢查值</li>
<li><strong>提供完整的依賴</strong>：讓 try 區塊能完整執行，而非依賴 catch 來「通過」測試。坑 3 的根因就是缺少收據產生器</li>
<li><strong>寫專門的失敗測試</strong>：故意製造失敗條件（如模擬印表機拋出 <code>PrinterException</code>），驗證錯誤處理行為符合預期</li>
</ul>
<h3 id="四fake--mock-的設計原則">四、Fake / Mock 的設計原則</h3>





<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">                    Fake（假實作）               Mock（模擬物件）
</span></span><span class="line"><span class="ln">2</span><span class="cl">─────────────────────────────────────────────────────────
</span></span><span class="line"><span class="ln">3</span><span class="cl">適用場景          需要跑通完整路徑               只需驗證互動次數/參數
</span></span><span class="line"><span class="ln">4</span><span class="cl">本案例            FakeReceiptBuilderService      不適用（需要驗證端到端結果）
</span></span><span class="line"><span class="ln">5</span><span class="cl">                  FakePrinterAdapter</span></span></code></pre></div><p>這次 <code>FakePrinterAdapter</code> 的設計漏掉了父類別繼承方法依賴的內部狀態（坑 2），<code>FakeReceiptBuilderService</code> 的回傳資料只覆蓋了部分分支（坑 4）。後來整理出設計 Fake 時的確認項目：</p>
<ul>
<li>繼承/實作的方法中，有哪些是上層呼叫者實際會用到的？</li>
<li>這些方法依賴哪些內部狀態（如 <code>late</code> 變數）？</li>
<li>Fake 的初始化是否正確建立了這些內部狀態？</li>
<li>Fake 回傳的資料是否足以讓下游所有分支都被觸發？</li>
</ul>
<hr>
<h2 id="之後可以改善的地方">之後可以改善的地方</h2>
<h3 id="寫測試時">寫測試時</h3>
<ul>
<li>測試應從入口方法開始驅動，讓中間的邏輯實際執行，避免手動構造中間結果</li>
<li>使用模擬子類別時，確認上層實際呼叫到的繼承方法也有被測試覆蓋</li>
<li>斷言驗證值本身，而非只驗證存在性（<code>containsKey</code> → 直接檢查值）</li>
<li>設計模擬元件的回傳資料時，先確認下游有哪些分支，確認回傳資料能觸發這些分支</li>
<li>回傳資料中加入邊界值——空字串、空列表等</li>
</ul>
<h3 id="修-bug-時">修 Bug 時</h3>
<ul>
<li>修完後確認有測試會實際走到修改的程式碼，否則測試通過不代表修改生效</li>
<li>沿著執行路徑往下看——之前被擋住的程式碼現在可以執行了，那些區段可能存在未發現的問題</li>
<li>如果修改讓新的資料流入第三方 library，檢查那些資料是否有 edge case</li>
</ul>
<h3 id="設計-try-catch-時">設計 try-catch 時</h3>
<ul>
<li>區分「預期的執行期錯誤」和「程式碼 bug」，只攔截前者</li>
<li>定義專用的 exception 類型（如 <code>PrinterException</code>），在 IO 邊界包裝，上層只 catch 這個類型</li>
<li>資料準備、邏輯運算等步驟不要放在 try-catch 裡面，讓錯誤直接拋出</li>
</ul>
<h3 id="自我檢查清單">自我檢查清單</h3>
<p>寫完測試後可以對照的問題：</p>
<ol>
<li>這個測試有走過真實的呼叫路徑嗎？還是只測了資料搬運？</li>
<li>斷言是驗證「值」還是只驗證「存在」？</li>
<li>所有依賴都有提供嗎？缺少的依賴會不會被 try-catch 吞掉？</li>
<li>模擬子類別覆寫的方法之外，繼承的方法有被測到嗎？</li>
<li>模擬元件回傳的資料有觸發下游的所有分支嗎？</li>
<li>邊界值（空字串、空列表、null）有出現在測試資料中嗎？</li>
<li>try-catch 的範圍是否只包含 IO 操作？資料準備和邏輯運算是否在 try 外面？</li>
<li>有寫反向測試（故意觸發錯誤）來確認理解了 Bug 的根因嗎？</li>
</ol>
<hr>
<h2 id="本案例的最終測試結構">本案例的最終測試結構</h2>





<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">28 tests
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">無廚房印表機時的基本行為（4 tests）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  └── 基本的 handler 行為，不需要廚房印表機
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">OnlineOrderRecord 模型（7 tests）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  └── 單元測試：狀態管理、applyPrintResult
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">FakePrinterAdapter（6 tests）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  ├── init / sendBytes — 基本功能
</span></span><span class="line"><span class="ln">11</span><span class="cl">  ├── printText after init — 驗證內部元件初始化（抓 Bug 1）
</span></span><span class="line"><span class="ln">12</span><span class="cl">  └── printText without init — 反向驗證
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">KitchenPrinterConfig（2 tests）
</span></span><span class="line"><span class="ln">15</span><span class="cl">  └── 單元測試：品名匹配邏輯
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">品項分派邏輯 — 整合測試（4 tests）     ← 全部重寫
</span></span><span class="line"><span class="ln">18</span><span class="cl">  ├── 2 台空 mapping → odd/even 分配     抓 Bug 2
</span></span><span class="line"><span class="ln">19</span><span class="cl">  ├── 1 台空 mapping → fallback
</span></span><span class="line"><span class="ln">20</span><span class="cl">  ├── 明確 mapping 優先匹配
</span></span><span class="line"><span class="ln">21</span><span class="cl">  └── kitchenItemIds 篩選
</span></span><span class="line"><span class="ln">22</span><span class="cl">  （全部透過 printAppendedOrder 驅動，有 FakeReceiptBuilderService）
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">PrintCenter 廚房印表機管理（5 tests）
</span></span><span class="line"><span class="ln">25</span><span class="cl">  └── 註冊、移除、初始化、向後兼容</span></span></code></pre></div><hr>
<h2 id="最終的修復">最終的修復</h2>
<table>
  <thead>
      <tr>
          <th>Bug</th>
          <th>修復方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bug 1</td>
          <td>模擬印表機的初始化方法補上內部元件的建立</td>
      </tr>
      <tr>
          <td>Bug 2</td>
          <td>品項分派的 fallback 邏輯改為：唯一一台無對應表 → 全部給它，多台 → 依規則分配</td>
      </tr>
      <tr>
          <td>Bug 3</td>
          <td>多欄表格列印前，自動將欄位比例正規化為符合 library 要求的總和 12</td>
      </tr>
      <tr>
          <td>Bug 4</td>
          <td>文字列印前加入空字串檢查，遇到空字串改用換行指令繞過 library 的問題</td>
      </tr>
      <tr>
          <td>設計改善</td>
          <td>定義 <code>PrinterException</code>，列印方法改為只攔截印表機故障，程式碼 bug 不再被靜默吞掉</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>系統化除錯方法論</title><link>https://tarrragon.github.io/blog/record/%E7%B3%BB%E7%B5%B1%E5%8C%96%E9%99%A4%E9%8C%AF%E6%96%B9%E6%B3%95%E8%AB%96/</link><pubDate>Fri, 26 Sep 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E7%B3%BB%E7%B5%B1%E5%8C%96%E9%99%A4%E9%8C%AF%E6%96%B9%E6%B3%95%E8%AB%96/</guid><description>&lt;h2 id="為什麼需要系統化除錯方法論">為什麼需要系統化除錯方法論&lt;/h2>
&lt;p>除錯不是試錯過程，是品質提升的系統性工程。隨機修復會產生隨機品質。系統化除錯產生一致的架構改善。&lt;/p></description><content:encoded><![CDATA[<h2 id="為什麼需要系統化除錯方法論">為什麼需要系統化除錯方法論</h2>
<p>除錯不是試錯過程，是品質提升的系統性工程。隨機修復會產生隨機品質。系統化除錯產生一致的架構改善。</p>
<p>當AI協作處理複雜程式問題時，系統化除錯方法論成為唯一的執行準則。模糊的修復策略會產生模糊的結果。明確的除錯方法論產生一致的品質改善。</p>
<h2 id="系統化除錯的本質">系統化除錯的本質</h2>
<h3 id="系統化除錯不是什麼">系統化除錯不是什麼</h3>
<p>系統化除錯不是：</p>
<ul>
<li><strong>症狀修復</strong>：不掩蓋警告，只找根本原因</li>
<li><strong>批量處理</strong>：不自動修復，只精確分析</li>
<li><strong>簡單先行</strong>：不從容易修的開始，只按風險優先級</li>
<li><strong>表面清理</strong>：不只消除警告，只完成未完成的設計</li>
</ul>
<h3 id="系統化除錯是什麼">系統化除錯是什麼</h3>
<p>系統化除錯是：</p>
<ul>
<li><strong>根因分析</strong>：明確區分未完成實作vs過度設計</li>
<li><strong>風險導向</strong>：按業務風險和架構影響排序修復</li>
<li><strong>主從分工</strong>：主線程管控進度，代理人執行修復</li>
<li><strong>品質提升</strong>：每次修復都強化程式設計完整性</li>
</ul>
<h2 id="除錯的第一原則根因分析優先">除錯的第一原則：根因分析優先</h2>
<h3 id="問題本質分類">問題本質分類</h3>
<p>每個unused警告都屬於以下三類之一：</p>
<h4 id="未完成實作">未完成實作</h4>
<ul>
<li><strong>識別</strong>：功能設計完整但驗證邏輯缺失</li>
<li><strong>處理</strong>：補完實作而非移除程式碼</li>
<li><strong>範例</strong>：測試中建立了secondImport變數但未驗證重複匯入行為</li>
</ul>
<h4 id="過度設計">過度設計</h4>
<ul>
<li><strong>識別</strong>：功能已完成但包含不必要的複雜性</li>
<li><strong>處理</strong>：移除多餘程式碼保持精簡設計</li>
<li><strong>範例</strong>：建立獨立服務實例但架構採用單例模式</li>
</ul>
<h4 id="程式碼風格問題">程式碼風格問題</h4>
<ul>
<li><strong>識別</strong>：邏輯正確但命名或結構不一致</li>
<li><strong>處理</strong>：重構改善可讀性和一致性</li>
<li><strong>範例</strong>：使用File物件但混用path字串操作</li>
</ul>
<h3 id="分析判斷標準">分析判斷標準</h3>
<ul>
<li>變數有明確的業務意圖 → 未完成實作</li>
<li>變數創建後立即被丟棄 → 過度設計</li>
<li>變數使用方式不一致 → 程式碼風格問題</li>
</ul>
<p>不存在「可能是」的情況。如果無法明確分類，則需要更深入的程式碼分析。</p>
<h3 id="範例完整的根因分析">範例：完整的根因分析</h3>
<h4 id="情境test檔案中unused變數-initialmemory">情境：test檔案中unused變數 &lsquo;initialMemory&rsquo;</h4>
<h5 id="錯誤分析">錯誤分析</h5>
<p>「這個變數沒用到，直接刪掉。」</p>
<h5 id="正確分析過程">正確分析過程</h5>
<ol>
<li><strong>變數意圖</strong>：記憶體效率測試的基線測量</li>
<li><strong>使用模式</strong>：建立但未在驗證邏輯中引用</li>
<li><strong>分類判斷</strong>：未完成實作（測試設計完整但驗證缺失）</li>
<li><strong>修復策略</strong>：補完基線比較邏輯而非移除變數</li>
</ol>
<h2 id="除錯的第二原則風險導向排序">除錯的第二原則：風險導向排序</h2>
<h3 id="檔案風險等級">檔案風險等級</h3>
<p>檔案修復必須按風險等級執行：</p>
<h4 id="高風險檔案立即修復">高風險檔案（立即修復）</h4>
<ul>
<li><strong>核心業務邏輯</strong>：Domain層實作檔案</li>
<li><strong>基礎設施元件</strong>：Database、Service、Repository</li>
<li><strong>關鍵測試</strong>：端到端測試、整合測試</li>
</ul>
<h4 id="中風險檔案計畫修復">中風險檔案（計畫修復）</h4>
<ul>
<li><strong>輔助功能</strong>：Utility、Helper類別</li>
<li><strong>測試工具</strong>：Mock、TestData產生器</li>
<li><strong>配置檔案</strong>：Configuration、Environment設定</li>
</ul>
<h4 id="低風險檔案可延後修復">低風險檔案（可延後修復）</h4>
<ul>
<li><strong>單元測試變數</strong>：純測試輔助變數</li>
<li><strong>範例程式碼</strong>：Demo、Sample實作</li>
<li><strong>文件產生器</strong>：Documentation工具</li>
</ul>
<h3 id="風險評估標準">風險評估標準</h3>
<ul>
<li>影響核心功能 → 高風險</li>
<li>影響開發效率 → 中風險</li>
<li>純粹警告清理 → 低風險</li>
</ul>
<p>每個檔案只能歸類到一個風險等級。無法分類表示需要進一步的架構分析。</p>
<h3 id="修復優先序執行規則">修復優先序執行規則</h3>
<ul>
<li>高風險檔案：立即修復，不考慮複雜度</li>
<li>中風險檔案：當前Sprint完成</li>
<li>低風險檔案：下個版本或維護期處理</li>
</ul>
<h2 id="除錯的第三原則主從分工模式">除錯的第三原則：主從分工模式</h2>
<h3 id="角色定義">角色定義</h3>
<p>系統化除錯採用明確的角色分工：</p>
<h4 id="主線程職責">主線程職責</h4>
<ul>
<li><strong>進度管控</strong>：追蹤修復狀態和整體進展</li>
<li><strong>策略決策</strong>：確定修復優先序和資源配置</li>
<li><strong>品質檢查</strong>：驗證修復結果符合品質要求</li>
<li><strong>工作記錄</strong>：更新工作日誌避免遺漏</li>
</ul>
<h4 id="代理人職責">代理人職責</h4>
<ul>
<li><strong>詳細分析</strong>：深入檢查程式碼設計意圖</li>
<li><strong>修復執行</strong>：實際編寫和修改程式碼</li>
<li><strong>測試驗證</strong>：確保修復後功能正常</li>
<li><strong>技術回報</strong>：提供修復細節和影響評估</li>
</ul>
<h3 id="協作執行規則">協作執行規則</h3>
<ul>
<li>主線程永不直接修復程式碼</li>
<li>代理人永不決定修復優先序</li>
<li>每修復一個檔案都必須更新工作日誌</li>
<li>所有修復決策都必須通過主線程確認</li>
</ul>
<h3 id="範例完整的協作流程">範例：完整的協作流程</h3>
<h4 id="情境發現5個檔案有unused警告">情境：發現5個檔案有unused警告</h4>
<h5 id="主線程執行">主線程執行</h5>
<ol>
<li><strong>風險評估</strong>：將5個檔案按業務風險分類</li>
<li><strong>優先排序</strong>：確定高風險檔案的修復順序</li>
<li><strong>委託分析</strong>：要求代理人分析第一個檔案</li>
<li><strong>進度追蹤</strong>：更新TodoList標記當前處理檔案</li>
</ol>
<h5 id="代理人執行">代理人執行</h5>
<ol>
<li><strong>根因分析</strong>：判斷unused變數屬於未完成實作vs過度設計</li>
<li><strong>修復實施</strong>：根據分析結果執行對應的修復策略</li>
<li><strong>結果驗證</strong>：執行靜態分析工具確認警告消除</li>
<li><strong>影響報告</strong>：回報修復內容和對整體架構的影響</li>
</ol>
<h5 id="主線程確認">主線程確認</h5>
<ol>
<li><strong>驗證結果</strong>：檢查靜態分析工具輸出確認修復成功</li>
<li><strong>更新記錄</strong>：在工作日誌中記錄修復成果</li>
<li><strong>繼續協作</strong>：標記完成並委託下一個檔案分析</li>
</ol>
<h2 id="品質標準">品質標準</h2>
<h3 id="修復完成的判斷標準">修復完成的判斷標準</h3>
<p>每個檔案修復完成必須滿足：</p>
<ul>
<li><strong>警告消除</strong>：靜態分析工具不再顯示該檔案的unused警告</li>
<li><strong>功能完整</strong>：所有測試通過，不引入新的錯誤</li>
<li><strong>架構一致</strong>：修復符合Clean Architecture分層原則</li>
<li><strong>文件更新</strong>：工作日誌記錄修復內容和影響</li>
</ul>
<h3 id="整體品質提升指標">整體品質提升指標</h3>
<ul>
<li><strong>警告減少率</strong>：unused警告數量持續下降</li>
<li><strong>功能完整性</strong>：修復過程中完成更多未完成的實作</li>
<li><strong>架構一致性</strong>：程式碼風格和設計模式更加統一</li>
<li><strong>可維護性</strong>：程式碼可讀性和邏輯清晰度提升</li>
</ul>
<h3 id="品質驗證機制">品質驗證機制</h3>
<ul>
<li>每個檔案修復後立即執行靜態分析工具驗證</li>
<li>定期檢查整體警告數量變化趨勢</li>
<li>記錄修復過程中發現的架構改善機會</li>
<li>確認每次修復都強化而非弱化程式品質</li>
</ul>
<h2 id="執行流程">執行流程</h2>
<h3 id="標準修復流程">標準修復流程</h3>
<ol>
<li>
<p><strong>問題評估</strong>
執行靜態分析工具識別所有unused警告</p>
</li>
<li>
<p><strong>風險分析</strong>
將含有警告的檔案按風險等級分類</p>
</li>
<li>
<p><strong>優先排序</strong>
確定高風險檔案的修復順序</p>
</li>
<li>
<p><strong>逐檔修復</strong>
按優先序對每個檔案執行：</p>
<ul>
<li>委託代理人詳細分析</li>
<li>根因判斷(未完成實作vs過度設計vs程式碼風格)</li>
<li>執行對應修復策略</li>
<li>驗證修復結果</li>
<li>更新工作記錄</li>
</ul>
</li>
<li>
<p><strong>整體驗證</strong>
確認警告總數下降且無新錯誤引入</p>
</li>
</ol>
<h3 id="修復策略對應表">修復策略對應表</h3>
<table>
  <thead>
      <tr>
          <th>根因類型</th>
          <th>修復策略</th>
          <th>驗證標準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>未完成實作</td>
          <td>補完功能實作</td>
          <td>變數在邏輯中被正確使用</td>
      </tr>
      <tr>
          <td>過度設計</td>
          <td>移除多餘程式碼</td>
          <td>功能完整但程式碼更簡潔</td>
      </tr>
      <tr>
          <td>程式碼風格</td>
          <td>重構改善一致性</td>
          <td>邏輯不變但可讀性提升</td>
      </tr>
  </tbody>
</table>
<h3 id="例外處理原則">例外處理原則</h3>
<ul>
<li><strong>分析器誤報</strong>：確認變數確實被使用後保持現狀</li>
<li><strong>架構衝突</strong>：優先解決架構問題後再處理警告</li>
<li><strong>測試失敗</strong>：立即修復測試問題，暫停警告修復</li>
<li><strong>複雜邊界</strong>：分解為更小的問題單位處理</li>
</ul>
<h2 id="成果評估">成果評估</h2>
<h3 id="量化指標">量化指標</h3>
<ul>
<li><strong>警告消除數量</strong>：已修復的unused警告總數</li>
<li><strong>警告減少率</strong>：相對於初始狀態的改善百分比</li>
<li><strong>檔案修復數量</strong>：完成修復的檔案總數</li>
<li><strong>架構改善項目</strong>：修復過程中完成的設計改善</li>
</ul>
<h3 id="質化評估">質化評估</h3>
<ul>
<li><strong>根因解決率</strong>：真正解決問題vs僅消除警告的比例</li>
<li><strong>架構一致性</strong>：程式碼風格和設計模式統一程度</li>
<li><strong>功能完整性</strong>：修復過程中完成的未完成實作數量</li>
<li><strong>可維護性提升</strong>：程式碼清晰度和邏輯簡潔性改善</li>
</ul>
<h3 id="實戰案例v0819成果">實戰案例：v0.8.19成果</h3>
<p><strong>量化成果</strong>：</p>
<ul>
<li>初始警告：49個</li>
<li>最終警告：25個</li>
<li>改善率：49.0%</li>
<li>修復檔案：7個高風險檔案</li>
</ul>
<p><strong>質化成果</strong>：</p>
<ul>
<li>補完3個未完成的功能實作</li>
<li>移除4處過度設計的複雜程式碼</li>
<li>統一5個檔案的程式碼風格</li>
<li>解決2個架構不一致問題</li>
</ul>
<h2 id="持續改進">持續改進</h2>
<h3 id="方法論優化">方法論優化</h3>
<p>系統化除錯方法論必須持續優化：</p>
<ul>
<li><strong>記錄邊界案例</strong>：遇到的特殊情況和處理方式</li>
<li><strong>更新風險分類</strong>：基於實戰經驗調整風險評估標準</li>
<li><strong>改進協作模式</strong>：優化主線程和代理人的分工效率</li>
<li><strong>補充修復策略</strong>：新增針對特定問題類型的處理方法</li>
</ul>
<h3 id="知識累積">知識累積</h3>
<p>每次系統化除錯的經驗都必須沉澱為方法論改進：</p>
<ul>
<li>成功的修復策略納入標準流程</li>
<li>失效的方法從規範中移除</li>
<li>新發現的問題模式補充到分類標準</li>
<li>協作過程中的效率改善點持續優化</li>
</ul>
<h2 id="結論">結論</h2>
<p>系統化除錯方法論是品質提升的執行標準。它的價值在精確，它的目的是完成設計。</p>
<p>每個修復都是一次架構改善。每個分析都是一次設計檢視。每個協作都是一次品質提升。</p>
<p>執行系統化除錯就是執行品質標準。遵循這個方法論，我們能持續強化程式架構完整性和設計一致性。</p>
<p>這是工程規範，確保每次除錯都提升而非妥協專案品質。</p>
<h2 id="延伸套用到-linux-系統除錯">延伸：套用到 Linux 系統除錯</h2>
<p>這套方法論是語言與領域無關的通則。把它落到 Linux 系統除錯這個具體領域——「讀權威狀態而非肉眼猜表象」的紀律、症狀到情境的分流、逐層定位——見 <a href="/blog/linux/debug/diagnosis-read-authoritative-state/" data-link-title="診斷心法：讀權威狀態，不靠肉眼猜表象" data-link-desc="Linux 上一個現象看起來像 A 卻可能是 B、想建立一套先讀權威狀態再下判斷的除錯紀律、避免看畫面就猜而猜錯時回來讀">Linux 除錯與診斷：診斷心法</a>。那裡用實機案例（把鎖屏誤判兩次的教訓）展示同一套系統化紀律在 Linux 現場長什麼樣。</p>]]></content:encoded></item></channel></rss>