語意級假後端與流程測試
單元測試的 stub 有一個結構性限制:它的回應由測試作者寫死,回放的是作者對後端的假設。當 bug 的成因正是「假設錯了」(後端合併資料時會重建子項並換掉全部 id、刪除會連帶釋放關聯的佔用資源),stub 驗證不出任何東西——假設與斷言出自同一人之手,永遠自洽(案例:T.C5 凍結參照失效)。
語意級假後端是針對這個限制的測試形態:一個持有狀態、模擬已證實後端行為的假件,讓多個前端服務對它走完整的互動鏈。業界 test double 分類裡,這對應 Fowler 定義的 fake——有狀態、可運作的簡化實作;「語意級」強調的是行為出處紀律。
與 stub 的差異
兩者的分界在狀態的歸屬:stub 的回應由測試作者逐條寫死,語意級假後端自己持有狀態、讓每個操作演變它。
| 由測試餵資料的 stub | 語意級假後端 | |
|---|---|---|
| 資料來源 | 測試作者在每條測試裡寫死 | 假後端持有狀態,隨操作演變 |
| 行為 | 固定回應 | 模擬後端動詞的效果(建立、重建、刪除、連帶變更) |
| 驗證標的 | 前端邏輯在「假設成立」時是否正確 | 前端多服務接力在「已證實的後端行為」下是否正確 |
| 結構盲區 | 假設本身錯誤、跨服務互動 | 後端行為的未知變化(見界限) |
「模擬後端動詞的效果」是關鍵:對「合併兩筆資料」這類操作,假後端在自己的狀態裡執行完整效果——子項全部重建、舊參照全部失效——與實測證實的真實後端一致。前端若依賴舊參照仍然有效,測試立刻紅。完整情節見 T.C5。至於值不值得為此建一個假後端,判準見文末「結構界限與適用判準」段。
本模組的遮蔽機制章批評「讓 mock 更逼真」是結構性錯誤——mock 終究是開發者理解的副本。語意級假後端與那條批評的差別在三點:模擬止於應用層行為,協議層仍歸 protocol integration test;每條行為有實測出處,而非理解的副本;配對的真實後端驗證測試承擔行為漂移。三點缺一,它就退化成該章批評的對象。
掛載形態:接縫選在哪一層
假後端與前端服務鏈的接縫由既有架構決定,判準是「服務怎麼碰到後端」:
- 多個服務共用同一個 API client 介面(請求都經過同一個抽象出口)→ 注入 in-process 假實作:實作同一個介面、在測試進程內持有狀態。成本最低、執行最快、離線可跑
- 服務各自組請求、直接發 HTTP(缺少共用抽象)→ 本地起一個假 server,把服務的目標位址指過去。多維護一層 server 的啟停與序列化,換到的是連傳輸層一起走過
- 兩種形態混雜(部分服務走 client、部分自己發)→ 先把散落的請求收斂到共用介面,再掛 in-process 假實作;接縫統一之後,假後端才看得到完整的互動鏈
兩種形態各有環境前提:假 server 形態要求服務的目標位址可設定——位址硬編碼時,先把 endpoint 抽成可設定再掛;in-process 注入則要求服務與測試跑在同一個測試進程內。
行為從哪裡來:實測一次、固化為資產
假後端的每一條行為都必須有出處——已證實,而不是「我覺得後端應該是這樣」。否則它只是一個更精緻的假設回放器。
取證方式依成本遞增:
- 讀後端回應中的診斷資訊(若有 query log 之類的除錯欄位,直接看它做了哪些寫入)
- 對開發環境的真實後端做一次探測(操作前後直接讀狀態比對)
- 前端埋 log 實機操作一次
證實後寫進假後端,並遵守配對慣例:假後端每一條行為假設,對應一條真實後端驗證測試(見真實後端驗證測試)。假後端負責讓流程測試快速、確定地跑;驗證測試負責在後端行為漂移時把警報拉響。
可測性閘門:編排住在哪裡
流程測試繞過 UI 皮層、從編排入口以下驅動整條服務鏈——業界詞彙裡接近 subcutaneous test,也常被歸入 integration test 的範疇。它要驅動「真實的編排」,而編排常常住在 UI 層的控制器裡。開工前先做一個 spike:控制器能不能在測試環境裡立起來——具體判定是能在測試環境建構出該控制器並呼叫其編排入口,平台與環境依賴以假件或空實作替換。
spike 走完會分出兩條路。立得起來——平台通道(行動端的原生橋接、web 的 browser API)可以 mock、背景服務可用 no-op 子類替換——測試就直接呼叫編排入口,零漂移。立不起來,剩下兩個選擇:把編排抽到服務層(重構),或在測試裡複製編排步驟並在兩處互相標註(接受漂移風險)。
這個閘門的答案決定整個套件的形態,值得用一條最小測試先驗證,而不是寫到一半才發現。
一併確立驗證邊界:對話框、畫面選取這類純 UI 互動不在流程測試範圍——測試直接呼叫 UI 收集完參數後的編排入口。外接裝置(第二螢幕、印表機)以可注入的假件攔在傳輸出口,斷言送出的資料流與時序(T.C9),列印則以假印表機計數斷言張數。
流程測試的典型劇本
一條劇本 = 一段跨服務的業務旅程,斷言散佈在每個階段的可觀察結果上:
- 佈置:假後端 seed 初始狀態(資料實體、關聯資源、參照目錄)
- 前端服務鏈啟動(以輪詢同步架構為例):同步列表 → 訂閱事件 → 輪詢建立差異比對基準
- 業務操作:呼叫真實編排入口,執行會改變後端狀態的操作(合併、拆分、刪除這類會重建或釋放資料的動詞)
- 斷言:假後端的狀態變化(後端該發生的事)、前端的狀態對齊(本地資料該還原或更新)、對外輸出的副作用(外送訊息序列、輸出計數)
- 追加一輪輪詢:驗證下一個週期把先前的變更辨識為已處理。差異比對基準是輪詢用來判斷「哪些是新資料」的上一輪快照;這一步防的是基準被污染的迴歸形態——空結果覆寫基準,下一輪就把同一筆資料誤判為新增而重複輸出(見 T.C6)
步驟 2 與 5 是輪詢同步架構的情境實例;推播型架構有等價的命題——驗證事件重放時前端只輸出一次、重送的同一事件被辨識為已處理。
隔離紀律是劇本成立的前提:每條流程測試重建假後端實例(或等效重置),劇本之間狀態互不滲漏;seed 用共用 builder 組裝,初始狀態的形狀集中維護,避免每條測試手拼。
價值實例:這個形態的套件在建置過程中,首跑就抓到修復自身引入的順序 bug(T.C6);一條劇本在「單跑綠、合跑紅」的合跑階段暴露 fire-and-forget(呼叫後不等待)編排的時序競態(T.C8);雙行為開關(假後端同時模擬後端會做與不會做兩種行為、由開關切換)配合真實後端驗證測試,為一個先前只能互相猜測的前後端責任問題提供歸因定案的手段(T.C7)——開關是歸因期對「已證實」規則的暫時例外(其中一種行為尚未證實),定案後收斂回單一已證實行為。
結構界限與適用判準
這套形態的前提是後端無法在本機或容器裡啟動、只有共用測試環境可用(真實後端驗證測試在同一個前提下運作)。前提不成立時,成熟工具鏈有更直接的選項:
- 後端可容器化啟動 → 直接起真後端(testcontainers 類工具),假設由真實行為檢驗
- provider 團隊可配合 → consumer-driven contract test(如 Pact)讓後端在自己的 CI 裡驗證前端的假設
- 互動是線性單劇本 → 錄放式(record / replay)工具即可覆蓋
三者皆否、且需要有狀態的多劇本接力時,才輪到自建語意級假後端——錄放式的 cassette 是線性回放,做不到狀態接力。
語意級假後端固化的是「已知」。後端若默默改變行為,假後端不會跟著變,流程測試照樣綠——這個洞由配對的真實後端驗證測試補上。兩者合起來才是完整的形態:
- 假後端+流程測試:快、確定、可在離線與 CI 執行,防「前端改壞已知正確的流程」
- 真實後端驗證測試:慢、需環境,防「後端行為漂移讓已知變成過期」
建置與否用兩個自查問題判斷:專案裡是否有多個前端服務對同一份後端狀態接力?是否已經出現「對後端行為假設錯誤」型的漏網 bug?兩者都是 → 值得建。單一服務的 CRUD 驗證,由測試餵資料的 stub 就足夠,假後端持有狀態的維護成本高於它換到的增量。成本量級拆解:初版包含狀態模型(定義假後端持有的資料實體)、最先固化的 2-3 條已證實行為、加上一條端到端劇本——工作量主要取決於需要模擬的動詞數,2-3 個動詞是天級,超過 10 個動詞時先從最高風險的子集開始、其餘增量補入。這兩個數字是經驗閾值,依動詞的狀態複雜度調整——牽涉關聯資源連動(刪除單據要連帶釋放佔用)的動詞,一個就抵得上數個純 CRUD 動詞。之後隨新證實的行為增量維護。持有成本還包括失敗定位:流程測試紅燈時,除錯半徑橫跨 seed、服務鏈、假後端狀態到斷言的整條鏈,定位成本高於單元測試。維護面的 tripwire:後端行為高頻變動的時期,假後端加驗證測試是雙份維護、成本隨變動頻率放大——變動頻繁到每次迭代都要改兩處時,重新評估這套形態。
建置起步順序
決定建假後端後,按以下順序推進:
- 可測性閘門 spike:用一條最小測試驗證編排的宿主(控制器或服務入口)能否在測試環境立起來——這一步的答案決定後續全部形態(Dart/Flutter 生態的平台耦合中和手段見流程測試基礎設施)
- 掛載形態決定:依服務碰後端的方式(共用 client 介面 / 各自發 HTTP)選 in-process 注入或假 server
- 狀態模型:定義假後端持有的資料實體與初始 seed builder
- 第一條已證實行為:對真實後端取證(診斷資訊 / 探測 / 埋 log),固化為假後端的第一個 handler
- 第一條劇本:用 seed + handler 走完一段跨服務業務旅程,斷言每階段的可觀察結果
- 配對驗證測試:在真實後端驗證測試補一條對應斷言
步驟 4-6 形成增量循環:每證實一條新行為,假後端加一個 handler、流程測試加一條劇本、驗證測試加一條斷言。
下一步路由
- 配對的另一半 → 真實後端驗證測試
- stub 回放假設的完整案例 → T.C5 凍結參照失效被 stub 遮蔽
- 測試該寫成什麼樣子 → 測試註解與命名紀律
- Dart/Flutter 生態的實作限制(headless 立控制器、binding 互斥、假後端序列化)→ 流程測試基礎設施
#testing #fake-backend #flow-test #strategy #integration-test