<?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>Screen-State-Test on Tarragon</title><link>https://tarrragon.github.io/blog/tags/screen-state-test/</link><description>Recent content in Screen-State-Test 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/screen-state-test/index.xml" rel="self" type="application/rss+xml"/><item><title>三層定義與職責表</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/three-layer-definition/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/three-layer-definition/</guid><description>&lt;p>測試分層的目的是讓每一層只負責一類問題，使得「哪種 bug 該被哪層抓到」有明確歸屬。三層之間存在語意斷層，單靠一層無論寫多少 test 都無法跨越另一層的職責。&lt;/p>
&lt;h2 id="三層的職責邊界">三層的職責邊界&lt;/h2>
&lt;h3 id="unit-test驗證程式碼邏輯">Unit Test：驗證程式碼邏輯&lt;/h3>
&lt;p>Unit test 驗證的對象是「開發者寫的程式碼是否按預期運作」。它的輸入和輸出都在程式碼控制範圍內 — 函式的參數、回傳值、狀態變化、例外拋出。&lt;/p>
&lt;p>Unit test 的盲區是所有程式碼以外的東西。外部服務的協議行為、網路傳輸的編碼方式、作業系統的檔案鎖定機制 — 這些不在 unit test 的驗證範圍內，因為 unit test 用 mock 取代了這些外部依賴。Mock 忠實模擬的是程式語言層面的 API 契約（方法簽名、參數型別、回傳值），不是外部服務的協議行為。&lt;/p>
&lt;p>app_tunnel 的 192 個 unit test 全部通過，但實機連線後鍵盤輸入無回應。原因是 WebSocket 的 text frame 與 binary frame 差異屬於協議層語意 — &lt;code>FakeWebSocketChannel&lt;/code> 的 &lt;code>sink.add(dynamic)&lt;/code> 接受任何型別，不區分 frame type（&lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1&lt;/a>）。192 個 test 驗證的是「Dart 程式碼邏輯正確」，沒有任何一個 test 的職責是驗證「ttyd 收到的 frame type 是否正確」。&lt;/p>
&lt;h3 id="protocol-integration-test驗證真實協議互動">Protocol Integration Test：驗證真實協議互動&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test&lt;/a> 驗證的對象是「程式碼和真實外部服務之間的協議互動是否正確」。它不用 mock，而是對真實的服務實例發送請求，觀察真實的回應。&lt;/p>
&lt;p>這一層的驗證目標包括：連線握手是否完成、認證流程是否正確、資料編碼是否符合對方期望、逾時行為是否合理。這些問題的答案不在程式碼裡，而是在程式碼與外部服務的互動過程中。&lt;/p>
&lt;p>app_tunnel 的 auth handshake 缺失就是典型案例。ttyd 要求連線後發送 auth token JSON frame，但 &lt;code>ConnectionManager&lt;/code> 沒有實作這個步驟 — &lt;code>FakeWebSocketChannel.ready&lt;/code> 立即完成不需認證，所有 test 看到的都是連線成功（&lt;a href="https://tarrragon.github.io/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2&lt;/a>）。對真實 ttyd 執行一個「連線後不發 auth token，斷言 timeout」的 test，就能暴露這個缺失。&lt;/p>
&lt;h3 id="screen-state-test驗證畫面狀態完整性">Screen State Test：驗證畫面狀態完整性&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">Screen state test&lt;/a> 驗證的對象是「使用者可見的畫面狀態是否覆蓋所有情境」。它的關注點是畫面層級的狀態機 — loading、connected、error、reconnecting 等狀態之間的轉換是否完整，每個狀態下使用者看到什麼、能操作什麼。&lt;/p>
&lt;p>Screen state test 和 unit test 的區別在於斷言對象：unit test 斷言「函式回傳值是否正確」，screen state test 斷言「使用者看到的畫面是否正確」。同一段程式碼邏輯可能 unit test 通過（回傳值正確）但 screen state test 失敗（畫面沒顯示對應狀態），因為 UI 層的 binding 有問題。&lt;/p>
&lt;h2 id="三層對照">三層對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Unit Test&lt;/th>
 &lt;th>Protocol Integration Test&lt;/th>
 &lt;th>Screen State Test&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>驗證對象&lt;/td>
 &lt;td>程式碼邏輯&lt;/td>
 &lt;td>程式碼與真實服務的協議互動&lt;/td>
 &lt;td>使用者可見的畫面狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部依賴&lt;/td>
 &lt;td>全部 mock&lt;/td>
 &lt;td>對真實服務實例&lt;/td>
 &lt;td>視實作而定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>斷言標的&lt;/td>
 &lt;td>回傳值、狀態變化、例外拋出&lt;/td>
 &lt;td>連線結果、回應內容、逾時行為&lt;/td>
 &lt;td>畫面元素、狀態轉換、可操作性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>能抓到&lt;/td>
 &lt;td>邏輯錯誤、邊界條件、狀態機&lt;/td>
 &lt;td>協議不相容、認證缺失、編碼錯誤&lt;/td>
 &lt;td>狀態遺漏、轉換缺失、顯示錯誤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>抓不到&lt;/td>
 &lt;td>協議層行為、環境差異&lt;/td>
 &lt;td>UI 層 binding、畫面狀態完整性&lt;/td>
 &lt;td>內部邏輯錯誤、效能問題&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="數量與覆蓋率的關係">數量與覆蓋率的關係&lt;/h2>
&lt;p>測試數量和測試覆蓋率是兩個獨立的維度。192 個 unit test 提供的是 unit test 層的覆蓋率 — 程式碼邏輯的分支覆蓋。把 unit test 從 192 個加到 500 個，增加的仍然是同一層的覆蓋率，不會跨越到協議層或畫面層。&lt;/p></description><content:encoded><![CDATA[<p>測試分層的目的是讓每一層只負責一類問題，使得「哪種 bug 該被哪層抓到」有明確歸屬。三層之間存在語意斷層，單靠一層無論寫多少 test 都無法跨越另一層的職責。</p>
<h2 id="三層的職責邊界">三層的職責邊界</h2>
<h3 id="unit-test驗證程式碼邏輯">Unit Test：驗證程式碼邏輯</h3>
<p>Unit test 驗證的對象是「開發者寫的程式碼是否按預期運作」。它的輸入和輸出都在程式碼控制範圍內 — 函式的參數、回傳值、狀態變化、例外拋出。</p>
<p>Unit test 的盲區是所有程式碼以外的東西。外部服務的協議行為、網路傳輸的編碼方式、作業系統的檔案鎖定機制 — 這些不在 unit test 的驗證範圍內，因為 unit test 用 mock 取代了這些外部依賴。Mock 忠實模擬的是程式語言層面的 API 契約（方法簽名、參數型別、回傳值），不是外部服務的協議行為。</p>
<p>app_tunnel 的 192 個 unit test 全部通過，但實機連線後鍵盤輸入無回應。原因是 WebSocket 的 text frame 與 binary frame 差異屬於協議層語意 — <code>FakeWebSocketChannel</code> 的 <code>sink.add(dynamic)</code> 接受任何型別，不區分 frame type（<a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1</a>）。192 個 test 驗證的是「Dart 程式碼邏輯正確」，沒有任何一個 test 的職責是驗證「ttyd 收到的 frame type 是否正確」。</p>
<h3 id="protocol-integration-test驗證真實協議互動">Protocol Integration Test：驗證真實協議互動</h3>
<p><a href="/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test</a> 驗證的對象是「程式碼和真實外部服務之間的協議互動是否正確」。它不用 mock，而是對真實的服務實例發送請求，觀察真實的回應。</p>
<p>這一層的驗證目標包括：連線握手是否完成、認證流程是否正確、資料編碼是否符合對方期望、逾時行為是否合理。這些問題的答案不在程式碼裡，而是在程式碼與外部服務的互動過程中。</p>
<p>app_tunnel 的 auth handshake 缺失就是典型案例。ttyd 要求連線後發送 auth token JSON frame，但 <code>ConnectionManager</code> 沒有實作這個步驟 — <code>FakeWebSocketChannel.ready</code> 立即完成不需認證，所有 test 看到的都是連線成功（<a href="/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2</a>）。對真實 ttyd 執行一個「連線後不發 auth token，斷言 timeout」的 test，就能暴露這個缺失。</p>
<h3 id="screen-state-test驗證畫面狀態完整性">Screen State Test：驗證畫面狀態完整性</h3>
<p><a href="/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">Screen state test</a> 驗證的對象是「使用者可見的畫面狀態是否覆蓋所有情境」。它的關注點是畫面層級的狀態機 — loading、connected、error、reconnecting 等狀態之間的轉換是否完整，每個狀態下使用者看到什麼、能操作什麼。</p>
<p>Screen state test 和 unit test 的區別在於斷言對象：unit test 斷言「函式回傳值是否正確」，screen state test 斷言「使用者看到的畫面是否正確」。同一段程式碼邏輯可能 unit test 通過（回傳值正確）但 screen state test 失敗（畫面沒顯示對應狀態），因為 UI 層的 binding 有問題。</p>
<h2 id="三層對照">三層對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Unit Test</th>
          <th>Protocol Integration Test</th>
          <th>Screen State Test</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗證對象</td>
          <td>程式碼邏輯</td>
          <td>程式碼與真實服務的協議互動</td>
          <td>使用者可見的畫面狀態</td>
      </tr>
      <tr>
          <td>外部依賴</td>
          <td>全部 mock</td>
          <td>對真實服務實例</td>
          <td>視實作而定</td>
      </tr>
      <tr>
          <td>斷言標的</td>
          <td>回傳值、狀態變化、例外拋出</td>
          <td>連線結果、回應內容、逾時行為</td>
          <td>畫面元素、狀態轉換、可操作性</td>
      </tr>
      <tr>
          <td>能抓到</td>
          <td>邏輯錯誤、邊界條件、狀態機</td>
          <td>協議不相容、認證缺失、編碼錯誤</td>
          <td>狀態遺漏、轉換缺失、顯示錯誤</td>
      </tr>
      <tr>
          <td>抓不到</td>
          <td>協議層行為、環境差異</td>
          <td>UI 層 binding、畫面狀態完整性</td>
          <td>內部邏輯錯誤、效能問題</td>
      </tr>
  </tbody>
</table>
<h2 id="數量與覆蓋率的關係">數量與覆蓋率的關係</h2>
<p>測試數量和測試覆蓋率是兩個獨立的維度。192 個 unit test 提供的是 unit test 層的覆蓋率 — 程式碼邏輯的分支覆蓋。把 unit test 從 192 個加到 500 個，增加的仍然是同一層的覆蓋率，不會跨越到協議層或畫面層。</p>
<p>層級缺失的問題無法用數量解決。如果整個 test suite 只有 unit test，即使覆蓋率 100%，protocol integration test 層和 screen state test 層的覆蓋率仍然是 0%。app_tunnel 的經驗是：在 unit test 層加更多 test 不會讓 frame type 問題浮現，因為 <code>FakeWebSocketChannel</code> 的行為在每一個 test 中都是一致的 — 一致地遮蔽了協議層差異。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Mock 如何在 API 層和協議層之間製造盲區 → <a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">Mock 遮蔽機制分析</a></li>
<li>如何辨認「名義 integration test」 → <a href="/blog/testing/01-test-strategy-layers/nominal-integration-test/" data-link-title="「名義 integration test」的識別與修正" data-link-desc="test 名稱含 integration 但核心依賴全用 fake — 如何辨認、為什麼有害、怎麼修正命名和測試策略">名義 integration test 的識別與修正</a></li>
<li>判斷自己的服務是否需要 protocol integration test → <a href="/blog/testing/01-test-strategy-layers/when-protocol-integration-test/" data-link-title="判斷原則：什麼時候需要 protocol integration test" data-link-desc="從服務架構特徵判斷是否需要 protocol integration test 的決策流程 — 協議複雜度、mock 寬鬆度、失敗靜默度三個維度">判斷原則：什麼時候需要 protocol integration test</a></li>
<li>三層測試如何對應畫面狀態矩陣 → <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一：畫面狀態機</a></li>
</ul>
]]></content:encoded></item></channel></rss>