引言:驗收測試在部署管線中的角色#
自動驗收測試(Automated Acceptance Testing)是部署管線中的關鍵階段,它將交付團隊帶到基本持續整合之外的層次。一旦建立了自動驗收測試,便能驗證應用程式是否滿足商業驗收標準(business acceptance criteria),亦即確認應用程式是否為使用者提供了有價值的功能。驗收測試通常針對每一個通過提交測試(commit tests)的軟體版本執行。

Figure 8.1: The acceptance test stage
驗收測試的定義#
- 個別驗收測試旨在驗證某個 story 或需求的驗收標準是否已被滿足
- 驗收標準可以是功能性的(functional)或非功能性的(nonfunctional),後者涵蓋容量、效能、可修改性、可用性、安全性等
- 當某個 story 相關的驗收測試全部通過,表示該需求既完整又可正常運作
- 整體驗收測試套件同時具備兩個功能:驗證應用程式交付了客戶期望的商業價值,以及防止回歸缺陷破壞既有功能
驗收測試 vs. 單元測試#
驗收測試是面向業務的(business-facing),而非面向開發人員的。它們針對在類 production 環境中運行的應用程式測試完整的使用者情境(user scenarios)。單元測試的目的是證明應用程式的某個單獨部分按照程式設計師的意圖運作,但這與證明使用者擁有他們所需的功能截然不同。
為什麼自動驗收測試不可或缺?#
手動驗收測試的代價#
- 每次發布都需要進行驗收測試以防止缺陷外洩,某些組織每次發布的手動驗收測試費用高達三百萬美元
- 手動測試通常在開發完成、即將發布時才進行,此時團隊壓力極大,通常沒有足夠時間修復發現的缺陷
- 修復複雜缺陷時,又有很高的機率引入更多的回歸問題
為什麼單元測試不夠#
有些敏捷社群的倡導者主張以全面的單元測試與元件測試取代驗收測試,搭配結對程式設計、重構和探索性測試。但這個論點有幾個缺陷:
- 無法證明商業價值:單元與元件測試不會測試使用者情境,無法發現使用者在一連串互動過程中出現的缺陷
- 無法防護大規模變更:大規模重構時,單元測試本身也需要大幅修改,喪失了作為功能守衛的能力;只有驗收測試能在此過程結束後證明應用程式仍然正常
- 擅長捕捉特殊類型的缺陷:執行緒問題、事件驅動應用程式的湧現行為、架構錯誤或環境配置問題——這些通過手動測試都很難發現,更不用說單元測試
- 減輕測試人員負擔:放棄自動驗收測試會讓測試人員不得不花更多時間在枯燥的手動回歸測試上
自動化的正面效應#
| 效應 | 說明 |
|---|---|
| 回饋迴路更短 | 每個通過 commit test 的建置都執行驗收測試,缺陷在成本較低時就被發現 |
| 促進協作 | 測試人員、開發人員、客戶需要密切合作來建立良好的自動驗收測試套件 |
| 推動良好架構 | 驗收測試在精心設計的應用程式上效果最好——需要薄 UI 層、能在開發機器與生產環境上執行 |
如何建立可維護的驗收測試套件#
建立可維護的驗收測試的首要條件是嚴謹的分析流程。驗收測試源自驗收標準,因此驗收標準必須以自動化為前提來撰寫,並遵循 INVEST 原則(Independent, Negotiable, Valuable, Estimable, Small, Testable)。品質不佳的驗收標準是難以維護的測試套件的主要來源。
驗收測試的分層架構#
自動驗收測試應始終採用分層設計:

Figure 8.2: Layers in acceptance tests
| 層級 | 名稱 | 職責 |
|---|---|---|
| 1 | 驗收標準層(Acceptance Criteria) | 以 Given... When... Then... 格式表達的商業需求 |
| 2 | 測試實作層(Test Implementation Layer) | 使用領域語言(domain language)的程式碼,不參考任何 UI 元素 |
| 3 | 應用程式驅動層(Application Driver Layer) | 知道如何與被測系統互動以執行操作並回傳結果 |
測試實作直接參考應用程式 API 或 UI 是極常見的反模式。只要 UI 發生微小變更,就會立刻破壞所有引用該 UI 元素的測試。這也是錄製-回放式(record-and-playback)測試工具所產生的主要問題——這類工具產出的測試與 UI 緊密耦合,因此極度脆弱。
建立驗收測試#
分析師與測試人員的角色#
- 業務分析師(Business Analyst):代表客戶和使用者,與客戶識別並優先排序需求,確保 story 交付預期的商業價值
- 測試人員(Tester):確保交付團隊的所有人都了解軟體的品質和 production-readiness,與客戶和分析師定義驗收標準,與開發人員撰寫自動驗收測試,並執行探索性測試等手動測試活動
迭代式開發中的分析流程#
在迭代式交付中,典型的流程如下:
- 分析師與測試人員、客戶共同定義驗收標準
- 實作前,分析師、測試人員和開發人員舉行啟動會議(kick-off meeting),說明需求、商業背景和驗收標準
- 測試人員與開發人員協商自動驗收測試的內容
- 開發人員實作時,如遇不清楚之處隨時諮詢分析師
- 完成後(所有單元測試、元件測試和驗收測試通過),向分析師、測試人員和客戶展示成果
- 確認滿足需求後,交由測試人員進行後續測試
sequenceDiagram
participant 分析師
participant 測試人員
participant 開發人員
participant 客戶
分析師->>測試人員: 共同定義驗收標準
分析師->>開發人員: 召開啟動會議
測試人員->>開發人員: 協商自動驗收測試
開發人員->>開發人員: 實作功能
開發人員-->>分析師: 諮詢業務需求
開發人員->>客戶: 展示成果
客戶-->>測試人員: 交付後續測試
測試人員->>測試人員: 探索性測試這些啟動會議是迭代式交付流程的重要黏合劑。它們防止分析師建立「象牙塔」需求、防止測試人員提出非缺陷的缺陷報告、防止開發人員實作出與實際需求無關的功能。
驗收標準作為可執行規格(Executable Specifications)#
這是行為驅動開發(Behavior-Driven Development, BDD)的核心理念:驗收標準應以客戶對應用程式行為的期望來撰寫,並能直接對應用程式執行以驗證其規格。
Chris Matts 和 Dan North 提出的 DSL 格式:
Given 某個初始情境,
When 某個事件發生,
Then 有某些結果.具體範例——金融交易應用程式:
Feature: Placing an order
Scenario: User order should debit account correctly
Given there is an instrument called bond
And there is a user called Dave with 50 dollars in his account
When I log in as Dave
And I select the instrument bond
And I place an order to buy 4 at 10 dollars each
And the order is successful
Then I have 10 dollars left in my accountCucumber、JBehave、Concordion、Twist、FitNesse 等工具允許將這類驗收標準以純文字形式撰寫,並與底層實作保持同步。可執行規格的重要優勢在於:它們永遠不會過時——如果規格不能準確描述應用程式的行為,執行時就會拋出異常。
應用程式驅動層(Application Driver Layer)#
應用程式驅動層是了解如何與被測系統溝通的那一層,其 API 以領域語言表達,可視為一種領域特定語言(Domain-Specific Language, DSL)。
以 JUnit 風格表達的同一個驗收測試:
public class PlacingAnOrderAcceptanceTest extends DSLTestCase {
@Test
public void userOrderShouldDebitAccountCorrectly() {
adminAPI.createInstrument("name: bond");
adminAPI.createUser("Dave", "balance: 50.00");
tradingUI.login("Dave");
tradingUI.selectInstrument("bond");
tradingUI.placeOrder("price: 10.00", "quantity: 4");
tradingUI.confirmOrderSuccess("instrument: bond",
"price: 10.00", "quantity: 4");
tradingUI.confirmBalance("balance: 10.00");
}
}應用程式驅動層的設計要點#
| 設計要點 | 說明 |
|---|---|
| 別名機制(Aliasing) | 測試中使用 Dave 或 bond 等別名,實際上驅動層會建立具有唯一識別碼的真實使用者和工具,每次測試執行時都重新建立。這使得測試完全獨立,可安全平行執行 |
| 預設值 | 大部分參數為可選,具有合理的預設值,只有必要的參數才需指定 |
| 可重用性 | 因為高度重用,複雜的互動和操作只需撰寫一次。若出現間歇性問題,只需在一處修復,所有重用此功能的測試都會受益 |
外部 DSL vs. 內部 DSL#
| 面向 | 外部 DSL(如 Cucumber) | 內部 DSL(如 JUnit) |
|---|---|---|
| 優點 | 驗收標準可雙向同步、非技術人員可讀 | 工具簡單、支援 IDE 自動完成、直接存取 DSL |
| 缺點 | 需額外工具與同步開銷 | 較難轉換為純文字文件、轉換是單向的 |
Window Driver 模式:將測試與 GUI 解耦#
若決定對 GUI 執行驗收測試,應使用 Window Driver 模式在測試與 GUI 之間提供一層抽象,降低測試對 GUI 變更的敏感度。

Figure 8.3: The use of the window driver pattern in acceptance testing
對 GUI 測試的挑戰#
| 挑戰 | 說明 |
|---|---|
| 快速變更 | UI 在開發過程中頻繁變動,微小的 UI 變更就可能破壞整個測試套件 |
| 情境設定複雜 | 透過 UI 設定測試的初始狀態可能需要大量互動 |
| 結果驗證困難 | UI 可能無法提供驗證測試結果所需的資訊 |
| 技術限制 | 某些 UI 技術極難進行自動化測試 |
Window Driver 的運作方式#
- 為 GUI 的每個部分撰寫一個對應的驅動程式(類似設備驅動程式)
- 測試程式碼只透過 window driver 與 GUI 互動
- 當 UI 變更時,只需修改 window driver 中的程式碼,所有依賴的測試保持不變
- 若提供新的 GUI(如將 Web 介面換成觸控螢幕介面),只需建立新的 window driver 並替換即可
若應用程式設計良好,GUI 層只包含純展示邏輯而不含商業邏輯,則可以繞過 GUI 直接對底層 API 進行測試,風險相對較小。這種策略只需要開發團隊有足夠的紀律,讓展示層專注於「畫素繪製」(pixel-painting),不涉入商業或應用邏輯。
實作驗收測試#
測試中的狀態管理#
驗收測試需要狀態來模擬使用者互動,但必須最小化對複雜狀態的依賴:
- 避免使用生產資料的備份來填充測試資料庫,改為維護最小的一致資料集
- 理想的測試應是原子性的(atomic)——自行建立所需的一切,測試結束後清理乾淨,且執行順序無關緊要
- 最有效的方法:利用應用程式自身的功能來隔離測試範圍。例如,若系統支援多使用者獨立帳戶,在每次測試開始時建立新帳戶
- 若不得已需要在測試之間共享狀態,測試必須極其謹慎地設計:使用前置條件斷言驗證初始狀態,以相對值而非絕對值進行斷言
流程邊界、封裝與測試#
- 自動測試確實要求程式碼更加模組化和更好的封裝,但不應為了可測試性而破壞封裝
- 應盡量避免建立只為驗證行為而存在的「後門」(back door)程式碼
- 若不得不提供後門,應僅限於外部元件的可控 stub 或 test double,絕不要在將部署到 production 的遠端系統元件上添加僅供測試使用的介面
使用測試替身(Test Doubles)#

Figure 8.4: Test doubles for external systems
自動驗收測試不應在包含所有外部系統整合的環境中執行。策略是雙管齊下:
- 為所有外部系統建立 test doubles,使測試環境可控
- 針對每個整合點建立小型的整合測試套件,在真實連線的環境中執行
Test doubles 的額外優勢:提供可以控制行為的點,可模擬通訊失敗、錯誤回應、負載下的回應等情境。
測試外部整合點#
- 整合測試應聚焦於你的系統與外部系統的具體互動,而非全面測試外部系統介面
- 從明顯的場景開始,隨著發現的問題逐步增加測試案例
- 執行時機需視情況而定——可以與驗收測試同時執行,也可以每天或每週一次,或作為部署管線中的獨立階段
管理非同步與逾時#
測試非同步系統時,核心問題是:測試是否失敗了,還是只是在等待結果? 策略是讓非同步事件在測試層面看起來是同步的:
| 策略 | 說明 |
|---|---|
| 固定延遲 | Wait(DELAY_PERIOD) 後檢查結果——簡單但慢,延遲會快速累積 |
| 輪詢重試 | 短暫暫停後反覆檢查結果,直到成功或超時——比固定延遲快得多(作者曾藉此將驗收測試從 2 小時縮減到 40 分鐘) |
| 事件監聽 | 若系統有事件機制,等待特定事件而非輪詢——最快但較複雜 |
這類非同步處理程式碼應放在應用程式驅動層中以便重用。其相對複雜度是值得的,因為它可以被調校得既高效又完全可靠,使所有依賴它的測試也同樣可靠。
驗收測試階段的實踐#
嚴格的通過/失敗門檻#
驗收測試階段的規則是:每個通過 commit test 的建置都必須執行驗收測試套件。與容量測試等後續階段不同(可由人類判斷是否允許通過),驗收測試不容任何妥協——通過就能繼續前進,失敗就永遠不能進入後續階段。
作者的經驗:沒有良好的自動驗收測試覆蓋,只有三種結果——在流程末期花大量時間找 bug、花大量金錢做手動測試、或發布品質低劣的軟體。
保持驗收測試為綠色#
- 驗收測試損壞時,團隊需立即停下來分類問題:是脆弱的測試?環境配置錯誤?假設不再有效?還是真正的失敗?
- 整個交付團隊(包括開發人員和測試人員)共同擁有驗收測試的維護責任,而非獨立的測試團隊
- 讓失敗可見——使用大型建置螢幕、熔岩燈等手段
- 若放任驗收測試腐壞,到了發布時期會無法分辨失敗的原因,最終測試被刪除或忽略,回到手動測試的老路
部署測試(Deployment Tests)#
在執行功能性驗收測試之前,先執行一組小型的部署測試(也稱為基礎設施測試或環境測試),確認:
- 自動部署到類 production 環境是否成功
- 各元件之間的通訊通道是否正確建立
- 環境配置是否符合預期
這組測試作為快速失敗機制——若部署測試失敗,立即中止整個驗收測試階段,無需等待冗長的測試套件完成。
flowchart TD
A[自動部署到測試環境] --> B[執行部署測試]
B --> C{部署測試通過?}
C -->|是| D[執行功能性驗收測試]
C -->|否| E[❌ 中止驗收測試階段]
D --> F{驗收測試通過?}
F -->|是| G[✅ 進入下一階段]
F -->|否| H[回報失敗]驗收測試效能#
驗收測試的首要目的是斷言系統行為,效能是次要考量。然而,縮短回饋時間仍很重要。以下是一系列逐步進階的優化技巧:
1. 重構共通任務#
- 追蹤最慢的測試,定期花時間優化
- 識別共通的設定模式,將其抽取到測試輔助類別(test helper classes)中
- 若有公開 API 可用於測試設定,優先使用 API 而非透過 UI
2. 共享昂貴資源#
- 在驗收測試開始時建立一個乾淨的系統實例,所有測試共用該實例,結束後關閉
- 權衡哪些資源在測試間共享、哪些在單一測試範圍內管理
3. 平行測試#
- 當測試的隔離性良好時,可對單一系統實例平行執行測試
- 對多使用者的伺服器端系統而言,這是明顯的加速手段
4. 使用運算叢集(Compute Grid)#

Figure 8.5: A specific example of compute grid for acceptance testing
- 結合虛擬伺服器,極限情況下每個測試可分配到獨立的主機上執行
- 可利用雲端運算(如 Amazon EC2)取得更廣泛的可擴展性
- 現代 CI 伺服器(如 Selenium Grid)提供了管理測試伺服器叢集的內建功能
作者的實際案例:先透過重構共通模式優化,再將 API 測試與 UI 測試分離(API 測試先行,失敗即中止),接著進行粗粒度的平行執行(將測試分批在不同虛擬機上執行),最後切換到 Amazon EC2 雲端以獲取更大的可擴展性。
總結#
自動驗收測試是交付流程中的重要投資,它將團隊的注意力聚焦在真正重要的事情上:使用者需要系統提供的行為。
驗收測試比單元測試更複雜、維護成本更高,也更容易處於損壞狀態。但作為從使用者角度對系統行為的保證,它們提供了無可取代的防禦,抵禦在任何複雜應用程式生命週期中必然出現的回歸問題。
採用驗收測試驅動的方法帶來的具體效益:
| 效益 | 說明 |
|---|---|
| 提升信心 | 軟體確實適合其目的 |
| 保護大規模變更 | 安全地進行大規模重構或架構調整 |
| 全面的自動回歸測試 | 顯著提升品質 |
| 快速可靠的回饋 | 缺陷發生時立即被發現和修復 |
| 釋放測試人員 | 讓測試人員專注於策略制定、可執行規格的開發、以及探索性和可用性測試 |
| 縮短交付週期 | 實現持續部署(continuous deployment) |