「要加一條 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 救不了它。
我們採用幾個保護措施:
- 每筆測試資料帶唯一標記與明確的測試命名。
- 正常路徑在
finally盡力清理。 - timeout 要涵蓋完整的建立、刪除與二次確認,不在中途切斷。
- 準備可獨立執行的殘留清理工具。
- 破壞性情境使用獨立 marker,必要時可從夜間套件排除。
刪除後的驗證也要小心。如果查詢 API 只回傳前十筆資料,在那份截斷清單上看不到測試項目,不能證明它真的被刪掉。更可靠的方法是再次對同一個唯一標記執行刪除,並驗證系統回報找不到。
可觀測性要在寫情境前準備
E2E 最浪費時間的情況不是失敗,而是失敗後沒有證據。若 webhook 已被去重層擋掉,回覆 sink、對話遙測和模型紀錄可能全部是空的;從結果看起來,很像服務完全沒有收到請求。
所以測試 harness 應該替每次執行產生唯一事件識別,並在逾時訊息中提供:
- 本次事件與 request ID。
- 是否收到 webhook。
- 是否產生回覆。
- 是否寫入遙測。
- 最後一個工具與 outcome。
- 建議先檢查的常見原因。
這些資訊讓失敗從「重跑看看」變成可以定位的工程問題。
逃生閥也需要被看見
外部服務事故或新斷言本身有錯時,部署流程需要暫時降級,例如只跑健康檢查、跳過特定外部情境,或關閉寫入 smoke。
但逃生閥如果悄悄生效,比沒有逃生閥更危險。每次跳過都應該在 log 與部署摘要中醒目顯示,讓團隊知道目前少了哪一層保護,並留下恢復條件。
可以帶走的原則
一套可信的 E2E 系統不是用真實程度排序,而是用問題性質分工:
- 純邏輯與跨服務契約盡量提早、快速地驗證。
- 部署後 smoke 只驗基礎鏈路,不依賴昂貴或易抖動的服務。
- 真實情境只承擔環境差異造成的風險。
- 模型選擇與提示詞退步使用獨立評測,不綁住每次部署。
- 寫入情境必須有唯一標記、清理策略與 timeout 設計。
- 斷言行為與結構,不斷言自然語言的表面形式。
- 失敗診斷與逃生閥都是測試系統的一部分。
E2E 的價值不是把所有東西從頭跑到尾,而是在真正需要真實環境的地方,提供低噪音、能定位、值得相信的答案。