
Architecture Journal
E2E 測試:AI 時代的自動化測試與驗收(七)如何保護關鍵使用者旅程、前端 API 與非同步流程?
上一篇:契約測試的三種方案:OpenAPI + Schemathesis、Pact 與 Spring Cloud Contract
前幾篇把測試責任逐層拆開:單元測試驗證規則,整合測試驗證元件接在一起後能不能運作,契約測試驗證服務之間約定的邊界沒有被悄悄改掉。
但這些測試即使全部通過,還是可能漏掉一個使用者真正會遇到的問題:
從登入、操作畫面、呼叫 API,到資料寫入、非同步處理與最後的結果呈現,整條流程真的走得完嗎?
這就是 E2E(End-to-End)測試要回答的問題。
E2E 不應該是「把所有測試再跑一次」,也不應該是測試金字塔最上面堆滿測試案例。它的價值在於,用少量測試保護少數最重要、跨越最多系統邊界的使用者旅程。
當 AI 可以快速產生更多程式碼與測試時,真正稀缺的,是團隊是否知道哪些旅程值得保護,以及什麼結果才算完成。
E2E 測試保護的是旅程,不是所有功能
把前幾篇的分層放在一起,責任大致可以這樣看:
| 測試關注點 | 主要問題 | 常見範圍 |
|---|---|---|
| 單元測試 | 這個規則算對嗎? | 函式、類別、單一決策 |
| 整合測試 | 元件接在一起能運作嗎? | 資料庫、佇列、序列化、設定 |
| 契約測試 | 兩個系統對邊界的約定一致嗎? | API、事件、Schema、錯誤格式 |
| E2E 測試 | 使用者的關鍵旅程走得完嗎? | 前端、後端、資料與部署拓撲 |
| 驗收測試 | 這次交付值得接受嗎? | 業務目標、使用者價值、風險 |
E2E 關心的是跨邊界後的整體結果。例如「使用者成功下單」可能同時經過:
- 瀏覽器載入前端頁面。
- 前端取得商品與庫存資料。
- API 驗證使用者並建立訂單。
- 訂單服務寫入資料庫、發布事件,背景工作處理付款或庫存流程。
- 前端重新取得訂單狀態並顯示結果。
單元、整合、契約測試可以分別替其中幾段提供信心,但它們無法單獨證明這些段落在實際拓撲中接起來後,仍然符合使用者的預期。完整旅程測試才會直接驗證這件事。
這裡也要區分 E2E 與驗收測試。E2E 通常是一種技術驗證方式;驗收測試則是在判斷交付是否符合產品或業務條件。一個驗收條件可能用 API、瀏覽器或人工操作驗證,不一定都要寫成 E2E。反過來,一條 E2E 流程也可能只是部署後的健康檢查,還沒有涵蓋完整的業務驗收。
為什麼 E2E 不能什麼都測
E2E 的範圍越大,成本通常會一起增加:
- 需要更多服務、資料庫、佇列與外部依賴。
- 執行時間較長,開發者等待回饋的成本較高。
- 失敗時不容易立即知道是哪一層出問題。
- 測試資料、環境狀態與非同步處理會增加不穩定因素。
- UI、API 與部署設定的任何一處改動,都可能讓流程需要維護。
因此,E2E 測試不適合拿來覆蓋每個欄位、每個錯誤訊息與每一種邊界輸入。這些細節應該由較低層的測試快速涵蓋。
更實際的做法是:先找出失敗代價最高的使用者旅程,再確認其中哪些跨服務風險無法由低層測試取代。剩下的案例交給單元、整合或契約測試。
這樣做不代表降低測試標準,而是把較昂貴的測試用在低層測試無法回答的問題上。
怎麼挑值得保護的旅程
一條旅程是否值得成為 E2E,通常可以從幾個角度判斷:
| 判斷角度 | 要問的問題 |
|---|---|
| 業務影響 | 這條流程失敗,會不會直接影響收入、留存或核心服務? |
| 跨邊界程度 | 它是否經過多個服務、資料儲存或非同步流程? |
| 失敗代價 | 問題是否可能直到使用者操作時才被發現? |
| 使用頻率 | 這是不是使用者最常走的主流程? |
| 代表性 | 通過這條流程,能否代表一整類重要能力仍然可用? |
例如,電商系統可以優先保護:
- 使用者可以登入並查看自己的訂單。
- 使用者可以把商品加入購物車並完成下單。
- 使用者付款後,訂單會進入正確狀態。
- 使用者可以在前端看到非同步處理完成的結果。
不一定需要為每一個商品篩選條件都建立一條完整 E2E。篩選規則可以由單元測試覆蓋,查詢與資料庫整合可以由整合測試覆蓋,API 欄位格式可以由契約測試覆蓋。
E2E 要留下的是「如果這裡壞掉,使用者就無法完成重要事情」的案例。
Exploratory Testing 用來發現風險,E2E 用來固定保護旅程
只寫 happy path,確實容易讓 E2E 看起來通過了,卻沒有覆蓋真正危險的情境。這時可以使用 Exploratory Testing,針對一條核心旅程探索權限、異常資料、非同步延遲、外部服務失敗、重試與恢復等風險。
例如,可以針對下單旅程進行一次限時探索,專注於付款失敗、重複送出,以及庫存於付款期間變更等情境。探索後,只有高風險且可重現的問題,才需要沉澱成自動化回歸測試。
但 Exploratory Testing 與 E2E 不是同一件事。E2E 描述要驗證的系統邊界與使用者旅程;Exploratory Testing 描述測試人員如何在未知風險中學習、提問與找問題。探索可以從瀏覽器、API 或其他測試層開始,不一定都要成為 E2E。
探索結果也不應全部直接轉成自動化 E2E。比較好的做法是先分類:
- 需求或業務規則不清楚:補回驗收條件。
- 高風險且可重現的缺陷:加入適當層級的回歸測試。
- 只有跨系統串接才看得出的問題:考慮加入 E2E。
- 低風險或一次性的觀察:保留探索紀錄即可。
換句話說,Exploratory Testing 用來發現未知風險;E2E 用來長期守住已確認的重要旅程。兩者分開管理,才能讓 E2E 保持少量、穩定,而且有明確目的。
用下單流程畫出一條關鍵旅程
延續前幾篇的訂單例子,一條「使用者下單並看到處理結果」的 E2E 旅程,可以先畫成下面這樣:
| 步驟 | 使用者或系統行為 | E2E 要觀察的結果 |
|---|---|---|
| 1 | 使用者登入 | 頁面顯示已登入狀態,後續請求使用正確身分 |
| 2 | 使用者選擇可購買商品 | 商品資訊與可用狀態能從實際服務取得 |
| 3 | 使用者送出訂單 | 頁面顯示訂單已建立,不是只顯示按鈕被點擊 |
| 4 | 訂單服務寫入資料並發布事件 | 訂單編號可被查詢,訂單進入可觀察的處理狀態 |
| 5 | 付款或庫存流程完成 | 訂單狀態符合這個測試環境的預期 |
| 6 | 使用者重新查看訂單 | 前端顯示與後端狀態一致 |
這條測試不需要斷言每一個 HTTP header,也不需要檢查資料庫的每一個欄位或直接驗證內部事件是否發布。那些是契約或整合測試更適合處理的責任。
E2E 應該斷言的是使用者能辨認的結果,以及足以代表旅程完成的業務狀態。例如:
- 頁面顯示訂單編號。
- 訂單狀態從「處理中」進入「已確認」。
- 重新整理頁面後,結果仍然存在。
- 失敗時顯示可理解的狀態,而不是畫面停在無限載入。
如果流程包含付款、寄信或第三方物流,測試不必在每次執行時連到真實服務。可以使用 sandbox、測試替身或可控的測試帳號,重點是保留這條旅程真正需要驗證的系統邊界。sandbox 可以驗證與第三方的實際整合;測試替身則適合驗證本系統面對成功、失敗或 timeout 時的行為。使用測試替身不代表已經驗證第三方服務本身,這部分仍需要另外安排 sandbox 或契約層級的檢查。
E2E 的執行入口,以及 deployment smoke
「E2E」不是只有一種執行入口;deployment smoke 則是另一個依目的與執行時機定義的測試類型。團隊可以依照要驗證的邊界,選擇 API 或瀏覽器作為入口。
API-level E2E
API-level E2E 直接透過公開 API 完成完整業務流程。例如登入、建立訂單、查詢訂單狀態。它跳過瀏覽器渲染,因此通常比 browser-level E2E 快,也比較容易在服務層定位問題。
它適合驗證:
- 多個後端服務串起來後,完整業務流程是否成立。
- 身分、資料、事件與非同步狀態是否能正確傳遞。
- 前端以外的使用者端,例如行動 App 或外部整合,是否仍能使用這條流程。
Browser-level E2E
Browser-level E2E 從使用者操作畫面開始,驗證瀏覽器、前端路由、API 呼叫與後端流程的連接。
它適合保護少數最重要的使用者旅程,但不適合把每個 UI 狀態都塞進去。定位元素時,應優先使用使用者看得到的角色、標籤或明確的測試識別,而不是依賴容易變動的 CSS 結構。Playwright 官方文件也建議以面向使用者的定位方式與自動等待、web-first assertions 來降低時序問題。[1]
Deployment smoke test
Deployment smoke test 通常在部署完成後執行,只驗證最小但關鍵的路徑,例如:
- 首頁可以載入。
- 使用者可以登入。
- 一個核心 API 可以成功回應。
- 一條最重要的讀取或寫入流程可以完成。
它不一定涵蓋完整業務流程,也不一定需要跑完整瀏覽器矩陣。它要快速回答的是:「這個環境目前還活著,而且最基本的入口沒有壞掉。」
這些做法可以同時存在。API-level E2E 驗證後端旅程,browser-level E2E 驗證使用者入口,deployment smoke 驗證部署後的最小可用性。deployment smoke 可以使用 API 或瀏覽器執行,也不一定涵蓋完整業務旅程。與其糾結名稱,每條測試都要清楚說明自己保護哪個風險。
E2E 的資料與環境隔離
很多 E2E 失敗,表面上看起來像產品問題,實際上是測試之間互相污染。
常見情況包括:
- 多個測試共用同一個帳號,前一個測試改變了後一個測試的資料。
- 測試依賴資料庫裡某筆長期存在的商品或訂單。
- 測試依賴固定的訂單編號、時間或排序結果。
- 測試清理資料失敗,下一次執行才暴露問題。
- 測試依賴一個沒有明確 readiness check 的服務。
一條 E2E 流程應該盡量具備自己的前置條件與識別方式:
- 建立專用測試使用者或測試租戶。
- 建立本次測試專用的商品、訂單或其他資料。
- 使用唯一的 correlation ID 或測試標籤。
- 執行完成後清理可清理的資料,或使用一次性的測試環境。
- 不依賴其他測試先跑完,也不依賴測試執行順序。
理想上,測試環境應該接近實際部署拓撲,但仍然要可控。資料可以是真實格式的測試資料,外部副作用則應該明確隔離,例如使用付款 sandbox 或不會寄出真實郵件的通知服務。
非同步流程不要用 sleep 撐過去
訂單、付款、事件與背景工作通常不是同步完成。最容易出現的寫法,是在測試裡加一個固定的 sleep(5000),期待五秒後結果就會出現。
這種寫法同時有兩個問題:
- 服務很快時,測試白白等待。
- 服務變慢時,五秒又不夠,測試仍然失敗。
更可靠的做法是等待可觀察的業務條件,例如輪詢訂單狀態直到進入預期狀態,或讓測試框架使用帶有 timeout 的 assertion。Playwright 的 web-first assertions 會在條件未滿足時自動重試,適合用來等待畫面上的最終狀態,而不是等待一段沒有業務意義的時間。[2]
示意上,測試應該表達「等待訂單變成已確認」:
test('customer can complete the order journey', async ({ page }) => {
// 建立測試資料與登入狀態的方式依專案而定。
await page.goto('/orders/new');
await page.getByLabel('商品').selectOption('test-product');
await page.getByRole('button', { name: '送出訂單' }).click();
await expect(page.getByTestId('order-status'))
.toHaveText('已確認', { timeout: 15_000 });
});
這段程式碼是表達測試意圖的示意碼,商品、送出訂單、order-status 與測試資料建立方式都必須依實際產品調整。它保留在文章裡的價值,是讓讀者看到 E2E assertion 應該對準「使用者可觀察的最終結果」,而不是複製一份看似可以直接執行的專案程式碼。
如果非同步狀態只能透過 API 觀察,也可以讓測試輪詢 API 的業務結果。重點仍然是等待明確條件,並讓 timeout 反映合理的系統上限,而不是任意加長等待時間。
E2E 失敗時,先判斷是不是 flaky
E2E 失敗不代表一定是產品 bug。收到失敗結果時,可以先按來源分類:
| 失敗類型 | 常見訊號 | 下一步 |
|---|---|---|
| 產品邏輯錯誤 | 相同資料與環境下穩定重現 | 回到負責該行為的服務或前端修正 |
| 環境未準備好 | 服務啟動慢、連線拒絕、依賴尚未 ready | 補 readiness check 或調整環境啟動流程 |
| 資料互相污染 | 單獨執行通過,平行或重跑失敗 | 隔離 fixture、帳號與測試資料 |
| 定位或時序問題 | 元素偶爾找不到、固定等待後仍不穩 | 改用穩定 locator 與條件式等待 |
| 外部依賴不穩 | 第三方 sandbox timeout 或限流 | 使用可控替身,或把外部依賴風險獨立監控 |
每次失敗至少應保留足夠的診斷資料:測試步驟、瀏覽器截圖、console 與 network log、請求 correlation ID,以及服務端 log。Playwright 的 Trace Viewer 可以在測試失敗後重看操作步驟、DOM、network 與 assertion 狀態;官方文件也提供在第一次 retry 時保留 trace 的設定方式。[3]
Retry 可以幫助我們判斷偶發性,但不能拿來掩蓋不穩定。若一個測試常常第一次失敗、第二次成功,這本身就是系統或測試設計需要處理的訊號。
AI 可以產生骨架,但人要決定通過條件
AI 很適合協助建立 E2E 的初稿,尤其是已有使用者故事、驗收條件與 API 文件的專案。它可以協助:
- 從使用者故事整理出可能的關鍵旅程。
- 把旅程轉成測試步驟與 fixture 骨架。
- 根據頁面結構提出 locator 候選。
- 產生失敗摘要、trace 與 log 的初步分類。
- 找出目前只有低層測試、但缺少完整旅程保護的功能。
但 AI 不應該自行決定:
- 哪些旅程是產品真正不能失敗的核心路徑。
- 非同步流程在什麼狀態才算完成。
- 付款、通知或資料建立等副作用可以做到什麼程度。
- 測試失敗時要阻擋部署,還是只發出警告。
- 某個測試只是 smoke,還是足以代表一條完整驗收條件。
這些決定需要產品、開發、測試與維運共同確認。AI 可以把測試寫得很快,但「通過」的定義仍然是團隊的責任。
E2E 應該怎麼放進 CI
很多團隊的實際做法是:CI 先跑完單元、整合與契約測試,部署到測試環境後,再由腳本執行 E2E。這是合理的部署後 E2E 模式,因為它驗證的是已經組合好的服務、設定、網路、佇列與外部依賴。
它的代價是回饋比較晚。問題可能要等到部署完成後才會被發現,甚至已經進入某個環境。因此,部署後 E2E 適合驗證實際部署拓撲;部署前則可以保留少量、快速的核心旅程,提早阻擋明顯回歸。
E2E 的執行策略可以依回饋速度與風險分層:
| 時機 | 建議執行內容 | 目的 |
|---|---|---|
| Pull Request | 單元、整合、契約測試,加上少量最關鍵的 API 或 browser journey | 快速阻擋核心流程回歸 |
| 合併後/部署到測試環境 | 較完整的服務旅程與主要瀏覽器 | 驗證整合環境中的跨邊界行為 |
| Nightly 或排程 | 更多旅程、瀏覽器版本與資料組合 | 找出較低頻但重要的回歸 |
| 部署到正式環境後 | 最小 deployment smoke | 確認新環境真的可用 |
| 發布前 | 與本次變更相關的高風險旅程 | 提供發布決策所需的信心 |
如果團隊目前的流程是 CI 完成、部署後再執行 E2E,不需要為了追求新的分層方式而全部重寫。比較重要的是確認腳本真的驗證了完整業務結果,而不是只檢查首頁或一個 API 有回應。若部署後 E2E 失敗,管線也要事先定義是阻擋後續發布、觸發 rollback,還是只發出警告。
不要把所有 E2E 都放進 Pull Request,也不要把所有 E2E 都延後到發布後。PR 上應該留下能快速回饋的核心路徑;較慢、較廣的矩陣可以排程執行,但失敗結果仍然要有明確的負責人與處理期限。
E2E 的執行結果最好能和部署版本、測試資料、環境與 commit 綁在一起。否則看到「下單流程失敗」時,團隊還要先猜是哪個版本、哪個環境或哪批資料造成的。
結語:E2E 的價值在於守住少數不能失敗的事
E2E 測試不是信心的總量,也不是測試案例數量的競賽。
它真正要做的是,守住幾條使用者最重要、跨越最多系統邊界的旅程,確認這些旅程在真實部署拓撲中仍然走得完。細節交給單元、整合與契約測試;環境健康交給 smoke test;產品是否值得交付,則回到驗收條件與人的判斷。
在 AI 加速產生程式碼的時代,E2E 的價值反而更清楚。它要確認系統仍然完成了使用者真正要完成的事;逐行程式碼是否被操作過,並不是它要回答的問題。
下一篇會從測試往驗收再往前走:當 AI 能參與需求拆解、實作與測試產生時,團隊要怎麼把「可以交付」定義成可檢查、可追蹤的驗收流程?
參考資料
-
[1] Playwright,〈Best Practices〉
-
[2] Playwright,〈Assertions〉
-
[3] Playwright,〈Trace Viewer〉