「要加一條 E2E」很容易成為測試系統裡最昂貴的預設答案。只要功能跨了服務、碰到模型或外部 API,團隊就把整段流程搬進瀏覽器或真實環境。久了之後,測試套件變慢、容易抖動,失敗時也很難知道是哪一層出了問題。

我們替 Twinco 建立部署後自動化測試時,採用的判準只有一句:

換一台機器、換一份環境設定、換一個反向代理設定,結果會不會改變?

不會改變的留在低層,快速而且每次執行;會改變的才進真實環境。這篇文章整理這套分層方式,以及幾個比增加測試數量更重要的細節。

先把責任切開

我們把測試責任分成幾個層級。名稱不重要,重要的是每層只回答一種問題。

層級 負責回答 執行時機
單元/功能 純邏輯與 mock 可決定的行為正不正確 每個 PR
契約/組態 服務之間的 schema、提示詞與設定是否一致 每個 PR、部署前
部署後 smoke process、路由、權限與基本讀寫是否可用 每次部署
部署後情境 真實環境的跨服務鏈路是否打得通 staging 部署後
模型與外部依賴回歸 模型選擇、搜尋與付費 API 是否仍符合預期 夜間或手動

這樣切分後,E2E 不再代表「所有東西都真的跑一次」,而是只負責低層無法證明的那一段。

契約測試擋住無聲漂移

跨服務錯誤不一定會立即爆炸。例如後端新增一個欄位、另一個服務仍使用舊名稱,可能只有特定方案或特定路徑才出問題;LLM 工具從集合中被移除,提示詞卻仍告訴模型它可以使用,也可能只呈現為模型表現變差。

這類錯誤最適合在 PR 階段用契約測試處理:

  • 資料庫 schema 與兩個服務使用的模型是否同步。
  • 工具集合、工具說明與「工具不可用時的解釋」是否一致。
  • 部署腳本需要的環境變數名稱是否仍存在。
  • 前後端交換的狀態與 enum 是否擁有同一份事實來源。

契約測試不需要啟動完整正式環境,卻能攔住最容易靜默漂移的交界。

Smoke 測試應該快且不花外部成本

部署後 smoke 的目標不是證明產品完全正確,而是證明這次部署沒有把基礎鏈路切斷。它應該避免呼叫 LLM 與付費外部 API,才能在每次部署後穩定執行。

適合 smoke 的項目包括:

  • 健康端點、資料庫與 Redis 連線。
  • 內部 API 的驗證與權限。
  • queue worker 是否存活。
  • webhook 是否能走到正確 handler。
  • 寫入測試能否建立資料並在 finally 清除。

smoke 失敗時,部署流程需要知道是否阻止切流、通知團隊,或自動回滾。只有健康端點回 200 不足以證明應用能工作;但把完整使用者旅程放進切流前,又會讓部署被外部服務抖動綁架。

真實情境只驗環境會改變的部分

staging 的部署後情境會走真實 webhook、服務間驗證、模型與必要資料源,但斷言保持在結構層級。例如:

  • 是否選到正確工具類別。
  • 工具狀態是否為成功或需要補資訊。
  • 回覆是否存在且不是系統錯誤。
  • 對話遙測是否能被關聯到同一個合成事件。

我們刻意不斷言完整句子、特定措辭、token 的精確數字或外部 API 回傳的當下資料。模型與資料源會自然變動,將這些值寫死只會製造假失敗。假失敗多到沒人看紅燈時,再完整的 E2E 都等於沒有測試。

唯一適合斷言的動態文字,是測試自己產生的唯一標記。例如建立一個帶隨機短碼的測試行程,就能確認回覆和後續刪除真的針對同一筆資料,而不是剛好命中使用者原本的內容。

模型行為和部署鏈路是兩種問題

「今天會下雨嗎?」應不應該選擇天氣工具,通常不會因為換一台主機而改變。它屬於提示詞與模型行為評測,不該塞進每次部署後的 E2E。

我們將固定案例放進獨立模型評測:每次記錄選擇結果,和基準比較,只在退步時警示。這類測試需要真實模型,會花 token,也可能因模型服務短暫波動而失敗,因此適合夜間與手動執行。

分界可以用兩個例子看懂:

  • 氣象資料 API 從 staging 主機叫不叫得動:部署後情境。
  • 這句話該不該叫氣象工具:模型行為評測。

若兩者混在一起,失敗時只會看到「天氣測試紅了」,卻不知道該查網路、金鑰、提示詞還是模型版本。

有寫入的 E2E 必須自行清理

建立再刪除的測試比唯讀測試更能驗證權限與資料流,但也可能留下垃圾。尤其程序若在「建立成功、刪除之前」被 timeout 強制終止,普通 teardown 救不了它。

我們採用幾個保護措施:

  1. 每筆測試資料帶唯一標記與明確的測試命名。
  2. 正常路徑在 finally 盡力清理。
  3. timeout 要涵蓋完整的建立、刪除與二次確認,不在中途切斷。
  4. 準備可獨立執行的殘留清理工具。
  5. 破壞性情境使用獨立 marker,必要時可從夜間套件排除。

刪除後的驗證也要小心。如果查詢 API 只回傳前十筆資料,在那份截斷清單上看不到測試項目,不能證明它真的被刪掉。更可靠的方法是再次對同一個唯一標記執行刪除,並驗證系統回報找不到。

可觀測性要在寫情境前準備

E2E 最浪費時間的情況不是失敗,而是失敗後沒有證據。若 webhook 已被去重層擋掉,回覆 sink、對話遙測和模型紀錄可能全部是空的;從結果看起來,很像服務完全沒有收到請求。

所以測試 harness 應該替每次執行產生唯一事件識別,並在逾時訊息中提供:

  • 本次事件與 request ID。
  • 是否收到 webhook。
  • 是否產生回覆。
  • 是否寫入遙測。
  • 最後一個工具與 outcome。
  • 建議先檢查的常見原因。

這些資訊讓失敗從「重跑看看」變成可以定位的工程問題。

逃生閥也需要被看見

外部服務事故或新斷言本身有錯時,部署流程需要暫時降級,例如只跑健康檢查、跳過特定外部情境,或關閉寫入 smoke。

但逃生閥如果悄悄生效,比沒有逃生閥更危險。每次跳過都應該在 log 與部署摘要中醒目顯示,讓團隊知道目前少了哪一層保護,並留下恢復條件。

可以帶走的原則

一套可信的 E2E 系統不是用真實程度排序,而是用問題性質分工:

  • 純邏輯與跨服務契約盡量提早、快速地驗證。
  • 部署後 smoke 只驗基礎鏈路,不依賴昂貴或易抖動的服務。
  • 真實情境只承擔環境差異造成的風險。
  • 模型選擇與提示詞退步使用獨立評測,不綁住每次部署。
  • 寫入情境必須有唯一標記、清理策略與 timeout 設計。
  • 斷言行為與結構,不斷言自然語言的表面形式。
  • 失敗診斷與逃生閥都是測試系統的一部分。

E2E 的價值不是把所有東西從頭跑到尾,而是在真正需要真實環境的地方,提供低噪音、能定位、值得相信的答案。