The Commit Stage#

簡介#

提交階段(Commit Stage) 始於專案狀態的變更——即對版本控制系統的一次提交,結束時產出的是:若失敗,則提供失敗報告;若成功,則產出一組 二進位產物(binary artifacts) 與可部署的組件,供後續測試與發布階段使用,同時附帶應用程式狀態報告。

理想情況下,提交階段應在 五分鐘內 完成,最多不超過 十分鐘

提交階段在部署管線中的角色至關重要:

  • 它是 發布候選版本(release candidate) 被創建的起點
  • 許多團隊在實作部署管線時,最先建置的就是提交階段
  • 當團隊實踐 持續整合(Continuous Integration) 時,實質上就是在建立提交階段
  • 它驅動良好的設計實踐,對程式碼品質與交付速度都有顯著影響

Figure 7.1: The commit stage

提交階段的運作流程如下:

  1. 某人將變更提交到版本控制的 主線(mainline / trunk)
  2. 持續整合伺服器偵測到變更,檢出原始碼
  3. 執行一系列任務:
    • 編譯(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):

  1. 團隊成員提交變更
  2. CI 伺服器執行提交階段
  3. 成功後,二進位檔、報告與元資料(metadata)儲存至產物儲存庫
  4. CI 伺服器取回提交階段產生的二進位檔,部署至類生產測試環境
  5. 執行 驗收測試(acceptance tests),重用提交階段的二進位檔
  6. 通過驗收測試後,標記發布候選版本狀態
  7. 測試人員可查看所有通過驗收測試的建置清單,一鍵部署至手動測試環境
  8. 測試人員執行手動測試
  9. 通過手動測試後,更新狀態
  10. CI 伺服器取回候選版本,部署至效能測試環境
  11. 執行 容量測試(capacity tests)
  12. 通過後更新狀態為「capacity-tested」
  13. 依此模式重複至管線所有階段完成
  14. 通過所有階段後,候選版本進入「ready for release」狀態
  15. 發布完成後,標記為「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) 是一種設計模式:物件之間的關係由外部建立而非內部建立。

CarEngine 為例:與其在 Car 內部自行建立 Engine,不如在建構時由外部注入。這樣可以:

  • 建立具有不同 EngineCar,無需修改 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 的關鍵區別)
  • 常見工具:MockitoJMockEasyMockRhinoNMockMocha
  • 優點:程式碼量大幅減少、易於隔離第三方程式碼、測試執行速度通常很快

最小化測試中的狀態#

  • 避免建立過於複雜、難以理解和維護的資料結構來支撐測試
  • 理想的測試應 快速設定、更快速拆除
  • 若測試看起來笨重且複雜,通常反映系統設計有問題
  • 持續關注測試環境的複雜度,將其視為程式碼結構需要改進的訊號

偽造時間(Faking Time)#

時間相關的行為(如日終處理、延遲等待、閏年邏輯)若綁定系統時鐘,將對單元測試策略造成災難。

策略:

  • 將時間資訊的需求抽象為獨立的類別(如 Clock
  • 透過 依賴注入 注入時間包裝器
  • 測試時可 stub 或 mock Clock 類別,完全控制時間行為
  • 確保測試執行時所有延遲為零,維持測試效能

務實的速度考量(Brute Force)#

開發者總希望最快的提交周期,但需與提交階段識別常見錯誤的能力取得平衡:

  • 目標時間:五分鐘以內為理想,十分鐘為上限
  • 超過十分鐘的後果:開發者提交頻率降低,且不再在意測試是否通過

加速提交測試的兩個技巧:

  1. 平行化:將測試套件拆分,在多台機器上平行執行(現代 CI 伺服器的 build grid 功能)。記住:運算資源便宜,人力昂貴
  2. 推遲慢速測試:將執行時間長且不常失敗的測試移至驗收測試階段(代價是延遲收到這些測試的回饋)

總結#

提交階段應專注於一件事:盡快偵測變更可能引入的最常見錯誤,並通知開發者以便迅速修復

  • 每當有人對應用程式的程式碼或配置進行變更時,都應執行提交階段
  • 每位開發團隊成員每天會觸發多次
  • 建立提交階段——自動化流程,在每次變更時建置二進位檔、執行自動化測試、產出度量指標——是邁向 持續整合 的最低要求
  • 提交階段提供了最大的投資報酬率:一種典範轉移(paradigm shift),讓你精確知道變更在何時破壞了應用程式,並能立即修復