The Commit Stage#
簡介#
提交階段(Commit Stage) 始於專案狀態的變更——即對版本控制系統的一次提交,結束時產出的是:若失敗,則提供失敗報告;若成功,則產出一組 二進位產物(binary artifacts) 與可部署的組件,供後續測試與發布階段使用,同時附帶應用程式狀態報告。
理想情況下,提交階段應在 五分鐘內 完成,最多不超過 十分鐘。
提交階段在部署管線中的角色至關重要:
- 它是 發布候選版本(release candidate) 被創建的起點
- 許多團隊在實作部署管線時,最先建置的就是提交階段
- 當團隊實踐 持續整合(Continuous Integration) 時,實質上就是在建立提交階段
- 它驅動良好的設計實踐,對程式碼品質與交付速度都有顯著影響

Figure 7.1: The commit stage
提交階段的運作流程如下:
- 某人將變更提交到版本控制的 主線(mainline / trunk)
- 持續整合伺服器偵測到變更,檢出原始碼
- 執行一系列任務:
- 編譯(Compile):若為編譯語言,對整合後的原始碼進行編譯
- 提交測試(Commit Tests):執行單元測試等快速測試
- 組裝(Assemble):建立可部署至任何環境的二進位檔
- 程式碼分析(Code Analysis):檢查程式碼庫的健康狀態
- 建立後續管線階段所需的其他產物(如資料庫遷移腳本、測試資料等)
提交階段的原則與實踐#
如果部署管線的目標之一是淘汰不適合上線的建置,那麼提交階段就是 門口的守衛。其主要目標是:產出可部署的產物,或 快速失敗(fail fast) 並通知團隊失敗原因。
提供快速且有用的回饋#
提交測試失敗通常可歸因於以下三種原因:
| 錯誤類型 | 說明 |
|---|---|
| 語法錯誤(Syntax Error) | 編譯語言中的編譯錯誤 |
| 語意錯誤(Semantic Error) | 導致一或多個測試失敗 |
| 環境配置問題(Environment / Configuration Problem) | 應用程式或其環境的配置出錯 |
錯誤越早被發現,修復就越容易——不僅因為改動仍記憶猶新,更因為搜索錯誤原因的範圍更小。如果開發者遵循頻繁提交的原則,每次變更範圍就很小,在提交階段快速識別的失敗只涉及開發者個人的變更。
關於「快速失敗」的常見誤解:不應在發現第一個錯誤時立即停止整個建置。正確做法是將提交階段分為多個任務(編譯、單元測試等),只有當錯誤阻止後續任務執行時(例如編譯失敗)才停止,否則應執行到底並提供 彙總報告(aggregated report),讓開發者一次修復所有問題。
許多現代 CI 伺服器提供 預先測試提交(pretested commit / preflight build) 功能,在變更簽入前就執行提交階段。若無此功能,開發者必須在提交前於本機編譯並執行提交測試。
什麼情況應導致提交階段失敗?#
傳統上提交階段在編譯失敗、測試中斷或環境問題時才會失敗。但作者提出更進階的思考:
- 綠色的提交階段可能是假陽性(false positive)——如果測試數量太少、程式碼品質差、編譯產生大量警告,僅僅「通過」並不代表品質合格
- 可以設定 閾值(threshold):例如單元測試覆蓋率低於 60% 時失敗,低於 80% 時標記為「警示」而非「通過」
- 棘輪策略(Ratcheting):若警告數量增加或未減少,則使提交階段失敗
- 可設定程式碼重複度超過預設限制時也視為失敗
提交階段失敗時,團隊規則是必須 立即停下手邊工作來修復。不要因為未經團隊共識的原因導致失敗,否則人們會停止認真對待失敗訊號,持續整合將因此瓦解。
細心維護提交階段#
提交階段包含建置腳本與測試執行腳本,這些腳本需要 與應用程式碼同等級的維護。
- 設計不良的建置系統會以指數級的方式增加維護成本,不僅消耗開發資源,更拖慢所有人的生產力
- 作者曾見過一個專案有 10,000 行 XML 的 Ant 腳本,需要一整個團隊專門維護建置系統
- 建置腳本應保持 模組化(modular):常用且不常變動的任務與頻繁修改的任務分離,不同管線階段使用獨立腳本
- 避免環境特定腳本:將環境配置與建置腳本本身分離
賦予開發者所有權#
- 交付團隊必須對提交階段(乃至整個管線基礎設施)有 擁有感(ownership)
- 日常變更(新增函式庫、配置檔等)應由開發者與運維人員共同完成,而非依賴建置專家
- 專家的價值在於建立良好的結構、模式和技術選型,並將知識 轉移給交付團隊
大型團隊使用 Build Master#
- 小型團隊(20-30 人以下)可自組織管理建置紀律
- 大型或分散團隊可設立 Build Master 角色:監督建置維護、推動並執行建置紀律
- Build Master 不應是永久角色,團隊成員應輪流擔任(例如每週輪換),這是重要的學習經驗
提交階段的產出#
提交階段的輸入是 原始碼(source code),輸出包含:
- 二進位產物(Binaries):整條管線中會重複使用的同一組二進位檔,最終可能發布給用戶
- 報告(Reports):
- 測試結果(用於診斷失敗)
- 程式碼分析報告:測試覆蓋率(test coverage)、循環複雜度(cyclomatic complexity)、複製貼上分析(cut and paste analysis)、傳入/傳出耦合(afferent / efferent coupling)等
產物儲存庫(Artifact Repository)#
提交階段的產出需要儲存在 產物儲存庫 中,而非版本控制系統。原因如下:
- 產物儲存庫只需保留部分版本——當發布候選版本未通過某階段後,即可清除其相關產物
- 必須能從已發布的軟體 追溯回 版本控制中的對應修訂版本;若將產物簽入版本控制,會因引入額外修訂版本而使追溯複雜化
- 良好的配置管理策略要求二進位建置過程 可重複(repeatable)——從同一修訂版本重新執行提交階段,應得到完全相同的二進位檔
大多數現代 CI 伺服器內建產物儲存庫,支援自動清除過期產物。也可使用專用的產物儲存庫(如 Nexus 或其他 Maven 風格的儲存庫管理器)來處理二進位檔,方便從開發機器存取而無需與 CI 伺服器整合。

Figure 7.2: The role of the artifact repository
發布候選版本的完整流程(Happy Path):
- 團隊成員提交變更
- CI 伺服器執行提交階段
- 成功後,二進位檔、報告與元資料(metadata)儲存至產物儲存庫
- CI 伺服器取回提交階段產生的二進位檔,部署至類生產測試環境
- 執行 驗收測試(acceptance tests),重用提交階段的二進位檔
- 通過驗收測試後,標記發布候選版本狀態
- 測試人員可查看所有通過驗收測試的建置清單,一鍵部署至手動測試環境
- 測試人員執行手動測試
- 通過手動測試後,更新狀態
- CI 伺服器取回候選版本,部署至效能測試環境
- 執行 容量測試(capacity tests)
- 通過後更新狀態為「capacity-tested」
- 依此模式重複至管線所有階段完成
- 通過所有階段後,候選版本進入「ready for release」狀態
- 發布完成後,標記為「released」
sequenceDiagram
participant 開發者
participant CI伺服器
participant 產物儲存庫
participant 測試環境
participant 測試人員
開發者->>CI伺服器: 提交變更
CI伺服器->>CI伺服器: 執行提交階段
CI伺服器->>產物儲存庫: 儲存二進位產物
CI伺服器->>測試環境: 部署到驗收測試環境
CI伺服器->>CI伺服器: 執行自動驗收測試
CI伺服器->>測試環境: 部署到手動測試環境
測試人員->>測試環境: 執行手動測試
測試人員->>CI伺服器: 標記通過
CI伺服器->>測試環境: 部署到容量測試環境
CI伺服器->>CI伺服器: 執行容量測試
CI伺服器->>CI伺服器: 標記為 Ready for Release
CI伺服器->>測試環境: 部署到生產環境提交測試套件的原則與實踐#
提交測試套件的絕大多數應由 單元測試(unit tests) 組成。單元測試最重要的兩個特性:
- 速度極快:有時甚至在套件不夠快時讓建置失敗
- 高覆蓋率:約 80% 是良好的經驗法則

Figure 7.3: Test automation pyramid (Cohn, 2009)
Mike Cohn 的 測試自動化金字塔(Test Automation Pyramid) 說明了測試結構的理想比例:
| 層級 | 測試類型 | 特性 |
|---|---|---|
| 底層 | 單元測試(Unit Tests) | 數量最多,執行最快,應在幾分鐘內完成 |
| 中層 | 服務測試(Service Tests) | 驗收測試的一部分 |
| 頂層 | UI 測試(UI Tests) | 數量最少,但因為需要完整執行系統,耗時最長 |
避免透過 UI 測試#
在提交測試中,建議 完全不透過 UI 進行測試。UI 測試的問題:
- 涉及太多元件與軟體層級,準備測試環境耗時
- UI 設計為人類操作速度,相對於電腦速度極為緩慢
- UI 測試更適合放在部署管線的 驗收測試階段
使用依賴注入(Dependency Injection)#
依賴注入(DI) 或 控制反轉(Inversion of Control, IoC) 是一種設計模式:物件之間的關係由外部建立而非內部建立。
以 Car 與 Engine 為例:與其在 Car 內部自行建立 Engine,不如在建構時由外部注入。這樣可以:
- 建立具有不同
Engine的Car,無需修改Car程式碼 - 測試時注入
TestEngine,將測試範圍限定在目標類別
避免存取資料庫#
單元測試 絕不應依賴資料庫。依賴資料庫的測試問題包括:
- 執行速度大幅下降
- 測試的 狀態性(statefulness) 影響重複執行
- 基礎設施設定複雜度增加
- 若難以從測試中抽離資料庫,代表程式碼的 分層(layering) 與 關注點分離(separation of concerns) 設計不佳
可以在提交測試中包含一到兩個簡單的 冒煙測試(smoke tests)——從驗收測試套件中選取高價值且常用的功能進行端對端測試,證明應用程式確實能夠執行。
避免單元測試中的非同步#
非同步行為使測試變得困難。最簡單的策略是 拆分測試:
- 一個測試執行到非同步斷點前
- 另一個測試從非同步斷點後開始
- 將原始的訊息發送技術包裝在自訂的介面(interface)中,用 stub 或 mock 驗證呼叫
- 依賴基礎設施(即使是記憶體內訊息系統)的測試屬於 元件測試(component tests),應歸入驗收測試階段
使用測試替身(Test Doubles)#
理想的單元測試聚焦於少數密切相關的程式碼元件。在良好封裝的系統中,測試位於關係網絡中間的物件可能需要大量設定,因此需要偽造(fake)與依賴物件的互動。
Stubbing(存根):
- 用模擬版本取代系統的某部分,提供 罐頭回應(canned responses)
- 不關心 stub 如何被呼叫
- 適用於大型元件與子系統的模擬
Mocking(模擬):
- 讓系統自動建立 stub,減少手動撰寫 stub 的工作量
- 可以驗證被測程式碼是否以 預期方式 與 mock 互動(這是 mock 與 stub 的關鍵區別)
- 常見工具:Mockito、JMock、EasyMock、Rhino、NMock、Mocha 等
- 優點:程式碼量大幅減少、易於隔離第三方程式碼、測試執行速度通常很快
最小化測試中的狀態#
- 避免建立過於複雜、難以理解和維護的資料結構來支撐測試
- 理想的測試應 快速設定、更快速拆除
- 若測試看起來笨重且複雜,通常反映系統設計有問題
- 持續關注測試環境的複雜度,將其視為程式碼結構需要改進的訊號
偽造時間(Faking Time)#
時間相關的行為(如日終處理、延遲等待、閏年邏輯)若綁定系統時鐘,將對單元測試策略造成災難。
策略:
- 將時間資訊的需求抽象為獨立的類別(如
Clock) - 透過 依賴注入 注入時間包裝器
- 測試時可 stub 或 mock
Clock類別,完全控制時間行為 - 確保測試執行時所有延遲為零,維持測試效能
務實的速度考量(Brute Force)#
開發者總希望最快的提交周期,但需與提交階段識別常見錯誤的能力取得平衡:
- 目標時間:五分鐘以內為理想,十分鐘為上限
- 超過十分鐘的後果:開發者提交頻率降低,且不再在意測試是否通過
加速提交測試的兩個技巧:
- 平行化:將測試套件拆分,在多台機器上平行執行(現代 CI 伺服器的 build grid 功能)。記住:運算資源便宜,人力昂貴
- 推遲慢速測試:將執行時間長且不常失敗的測試移至驗收測試階段(代價是延遲收到這些測試的回饋)
總結#
提交階段應專注於一件事:盡快偵測變更可能引入的最常見錯誤,並通知開發者以便迅速修復。
- 每當有人對應用程式的程式碼或配置進行變更時,都應執行提交階段
- 每位開發團隊成員每天會觸發多次
- 建立提交階段——自動化流程,在每次變更時建置二進位檔、執行自動化測試、產出度量指標——是邁向 持續整合 的最低要求
- 提交階段提供了最大的投資報酬率:一種典範轉移(paradigm shift),讓你精確知道變更在何時破壞了應用程式,並能立即修復