<?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/%E6%95%B4%E5%90%88%E6%B8%AC%E8%A9%A6/</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/%E6%95%B4%E5%90%88%E6%B8%AC%E8%A9%A6/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/%E6%B7%B7%E5%90%88%E6%B8%AC%E8%A9%A6%E7%AD%96%E7%95%A5%E6%A0%B9%E6%93%9A%E6%9E%B6%E6%A7%8B%E5%B1%A4%E7%B4%9A%E9%81%B8%E6%93%87%E6%B8%AC%E8%A9%A6%E6%96%B9%E6%B3%95/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E6%B7%B7%E5%90%88%E6%B8%AC%E8%A9%A6%E7%AD%96%E7%95%A5%E6%A0%B9%E6%93%9A%E6%9E%B6%E6%A7%8B%E5%B1%A4%E7%B4%9A%E9%81%B8%E6%93%87%E6%B8%AC%E8%A9%A6%E6%96%B9%E6%B3%95/</guid><description>&lt;p>開始實踐 TDD 時，我們遇到一個困惑的問題：什麼都測，還是只測部分？&lt;/p>
&lt;p>追求覆蓋率，會寫出大量測試 getter 和直接欄位映射的測試，維護成本高，保護力低。不管覆蓋率，又很難有信心說業務邏輯正確。&lt;/p>
&lt;p>答案是：測試策略跟著架構走。&lt;/p></description><content:encoded><![CDATA[<p>開始實踐 TDD 時，我們遇到一個困惑的問題：什麼都測，還是只測部分？</p>
<p>追求覆蓋率，會寫出大量測試 getter 和直接欄位映射的測試，維護成本高，保護力低。不管覆蓋率，又很難有信心說業務邏輯正確。</p>
<p>答案是：測試策略跟著架構走。</p>
<h2 id="問題的根源">問題的根源</h2>
<p>全面單元測試的問題是，重構 ViewModel 內部實作時，大量測試跟著壞掉，但行為根本沒變。全面 BDD 的問題是，Domain 層邊界條件很難透過業務語言的場景完整覆蓋。</p>
<p>不同層級的程式碼，用不同的測試方法。</p>
<h2 id="五層架構的測試分工">五層架構的測試分工</h2>
<p><strong>Layer 1（UI/Presentation）</strong> 只針對關鍵互動流程撰寫整合測試。判斷標準：流程失敗是否影響核心業務、是否需要多步驟操作、是否涉及金流或敏感資料。靜態展示頁面、簡單列表交給人工測試。</p>
<p><strong>Layer 2（Application/Behavior）</strong> 只針對複雜轉換邏輯撰寫單元測試。判斷標準：轉換是否包含條件判斷、計算邏輯、多個來源資料，或邏輯超過十行。簡單的 DTO 欄位直接映射，不需要獨立測試，由 UseCase 層間接覆蓋。</p>
<p><strong>Layer 3（UseCase）</strong> 所有業務場景都必須撰寫 BDD 測試，沒有例外。每個 UseCase 至少涵蓋一個正常流程、兩個異常流程、三個邊界條件。格式使用 Given-When-Then，只 Mock 外層依賴，使用真實的 Domain Entity。</p>
<p><strong>Layer 4（Interface）</strong> 不測試。介面只定義合約，沒有可測試的行為。</p>
<p><strong>Layer 5（Domain Implementation）</strong> 複雜業務規則必須撰寫單元測試。判斷標準：是否包含業務規則驗證、計算邏輯、狀態轉換、不變量檢查。Email 格式驗證、金額範圍、訂單狀態轉換都需要完整的單元測試。純資料容器的 Entity 不需要獨立測試。</p>
<h2 id="做決策的流程">做決策的流程</h2>
<p>確定程式碼屬於哪一層（目錄結構直接反映架構層級），然後問一個問題：</p>
<ul>
<li>UI 層：這是關鍵互動流程嗎？</li>
<li>Behavior 層：這裡有複雜轉換邏輯嗎？</li>
<li>UseCase 層：直接寫 BDD 測試。</li>
<li>Interface 層：不測試。</li>
<li>Domain 層：這裡有複雜業務規則嗎？</li>
</ul>
<p>答案是就寫，不是就跳過讓上層覆蓋。</p>
<h2 id="技術性測試項目">技術性測試項目</h2>
<p>不分層級都要納入：</p>
<ul>
<li>Null 值和空集合</li>
<li>邊界值（零、負數、最大值）</li>
<li>異常處理（網路錯誤、儲存失敗）</li>
<li>資料驗證（格式、範圍、必填欄位）</li>
</ul>
<p>這些容易被忽略，但往往是上線後出問題的地方。</p>
<h2 id="覆蓋率的意義">覆蓋率的意義</h2>
<p>這套策略讓覆蓋率指標更有意義。UseCase 層要求行為場景覆蓋率 100%，是所有業務場景都有測試，不是追求程式碼行數百分比。Domain 層複雜邏輯要求分支覆蓋率 100%，每個分支都代表一個業務決策。整體新增程式碼維持 80% 以上。</p>
<p>數字背後有實際意義，不是為了報告好看。</p>
<h2 id="測試的穩定性">測試的穩定性</h2>
<p>UseCase 層的 BDD 測試關注行為，重構內部邏輯只要業務行為沒變，測試就不需要動。Domain 層只有規則本身改變才需要更新。Behavior 層只測複雜的轉換邏輯，重構簡單映射不影響測試。</p>
<p>測試應該是開發的保護網，不是阻力。測試因為業務改變而失敗，那很好；因為重構而大量失敗，那是設計問題。</p>]]></content:encoded></item></channel></rss>