Martin Fowler 談軟件持續集成(Continuous Integration)

 

本文原文鏈接:http://martinfowler.com/articles/continuousIntegration.html
原作者:Martin Fowler & Matthew Foemmel
諸    者:透明
譯 者語:2002年1月23日,我們很榮幸的在 UMLCHINA組織的網上交流中聆聽了Martin Fowler先生的教誨。在 交流中,Martin Fowler向所有中國軟件開發者推薦了這篇文章:Continuous Integration(《持續集成》)。初讀之下,我便感覺到了它的分量,AgileChina的林星也稱讚:“其中的思想非常的好,大師就是大師。” 然 後,用了一週的時間,我終於把這篇文章翻譯出來,以饗讀者。

由於這 是Fowler先生送給全體中國軟件開發者的禮物,所以我絕對不敢獨佔。任何人都可以在任何地方隨意 轉載本文,但是在轉載時請保持本文完整性——包括標題、版權聲明、原文鏈接、譯者語……總之,請不要在轉 載的時候做任何改動或增刪。另外,如果能在轉載的時候順手給我一個mail,我會更加高興。

在任何軟件 開發過程中都有一個重要的部分:得到可靠的軟件創建(build)版本。儘管知道創建的重要 性,但是我們仍然會經常因爲創建失敗而驚訝不已。在這篇文章裏,我們將討論Matt(Matthew Foemmel)在 ThoughtWorks的一個重要項目中實施的過程,這個過程在我們的公司裏日益受到重視。它強調完全自動化的、可 重複的創建過程,其中包括每天運行多次的自動化測試。它讓開發者可以每天進行系統集成,從而減少了集成中 的問題。

ThoughtWorks 公司已經開放了CruiseControl軟件的源代碼,這是一個自動化持續集成的工具。此外,我們還 提供CruiseControl、Ant和持續集成方面的顧問服務。如果需要更多的信息,請與Josh Mackenzie [email protected])聯繫。

在軟件開發的領域裏有各種各樣的“最佳實踐”,它們經常被 人們談起,但是似乎很少有真正得到實現的。 這些實踐最基本、最有價值的就是:都有一個完全自動化的創建、測試過程,讓開發團隊可以每天多次創建他們 的軟件。“日創建”也是人們經常討論的一個觀點,McConnell在他的《快速軟件開發》中將日創建作爲一個最 佳實踐來推薦,同時日創建也是微軟很出名的一項開發方法。但是,我們更支持XP社羣的觀點:日創建只是最低 要求。一個完全自動化的過程讓你可以每天完成多次創建,這是可以做到的,也是完全值得的。

在這裏,我們使用了“持續集成 (Continuous Integration)”這個術語,這個術語來自於XP(極限編程) 的一個實踐。但是我們認爲:這個實踐早就存在,並且很多並沒有考慮XP的人也在使用着它。只不過我們一直用 XP作爲軟件開發過程的標準,XP也對我們的術語和實踐產生了深遠的影響。儘管如此,你還是可以只使用持續集 成,而不必使用XP的任何其他部分——實際上,我們認爲:對於任何切實可行的軟件開發活動,持續集成都是很 基本的組成部分。

實現自動化日創建需要做以下幾部分的工作:

  1. 將所有的源代碼保存在單一的地點,讓所有人都能從這裏獲取最新的源代碼(以及以前的版本)。
  2. 使創建過程完全自動化,讓任何人都可以只輸入一條命令就完成系統的創建。
  3. 使測試完全自動化,讓任何人都可以只輸入一條命令就運行一套完整的系統測試。
  4. 確保所有人都可以得到最新、最好的可執行文件。

所有這些都必須得到制度的保證。我們發現,向一個項目中引入這些制度需要耗費相當大的精力。但是,我 們也發現,一旦制度建立起來,保持它的正常運轉就不需要花多少力氣了。

===============================

持續集成的優點

描 述持續集成最大的難點在於:它從根本上改變了整個開發模式。如果沒有在持續集成的實踐環境中工作過,你很難理解它的開發模式。實際上,在單獨工作的時候, 絕大多數人都能感覺到這種氣氛——因爲他們只需 要與自己的系統相集成。對於許多人來說,“團隊開發”這個詞總讓他們想起軟件工程領域中的一些難題。持續 集成減少了這些難題的數量,代之以一定的制度。

持續集成最基本的優點就是:它完全避免了開發者們的“除蟲會議”——以前 開發者們經常需要開這樣的 會,因爲某個人在工作的時候踩進了別人的領域、影響了別人的代碼,而被影響的人還不知道發生了什麼,於是 bug就出現了。這種bug是最難查的,因爲問題不是出在某一個人的領域裏,而是出在兩個人的交流上面。隨着時 間的推移,問題會逐漸惡化。通常,在集成階段出現的bug早在幾周甚至幾個月之前就已經存在了。結果,開發 者需要在集成階段耗費大量的時間和精力來尋找這些bug的根源。

如果使用持續集成,這樣的bug絕大多數都可以在引入的 同一天就被發現。而且,由於一天之中發生變動的 部分並不多,所以可以很快找到出錯的位置。如果找不到bug究竟在哪裏,你也可以不把這些討厭的代碼集成到 產品中去。所以,即使在最壞的情況下,你也只是不添加引起bug的特性而已。(當然,可能你對新特性的要求 勝過了對bug的憎恨,不過至少你可以多一種選擇。)

到現在爲止,持續集成還不能保證你抓到所有集成時出現的bug。持續集成的排錯能力取決於測試技術,衆 所周知,測試無法證明已經找到了所有的錯誤。關鍵是在於:持續集成可以及時抓到足夠多的bug,這就已經值 回它的開銷了。

所以,持續集成可以減少集成階段“捉蟲”消耗的時間,從而最終提高生產力。儘管現在還不知道是否有人 對這種方法進行過科學研究,但是作爲一種實踐性的方法,很明顯它是相當有效的。持續集成可以大幅減少耗費 在“集成地獄”中的時間,實際上,它可以把地獄變成小菜一碟。

集成越頻繁,效果越好

持續集成有一個與直覺相悖的基本要點:經常性的集成比很少集成要好。對於持續集成的實踐者來說,這是 很自然的;但是對於從未實踐過持續集成的人來說,這是與直觀印象相矛盾的。

如果你的集成不是經常進行的(少於每天一次),那麼集成就是一件痛苦的事情,會耗費你大量的時間與精 力。我們經常聽見有人說:“在一個大型的項目中,不能應用日創建”,實際上這是一種十分愚蠢的觀點。

不過,還是有很多項目實踐着持續集成。在一個五十人的團隊、二十萬行代碼的項目中,我們每天要集成二 十多次。微軟在上千萬行代碼的項目中仍然堅持日創建。

持 續集成之所以可行,原因在於集成的工作量是與兩次集成間隔時間的平方成正比的。儘管我們還沒有具體 的衡量數據,但是可以大概估計出來:每週集成一次所需的工作量絕對不是每天集成的5倍,而是大約25倍。所 以,如果集成讓你感到痛苦,也許就說明你應該更頻繁地進行集成。如果方法正確,更頻繁的集成應該能減少你的痛苦,讓你節約大量時間。

持 續集成的關鍵是自動化。絕大多數的集成都可以而且應該自動完成。讀取源代碼、編譯、連接、測試,這 些都可以自動完成。最後,你應該得到一條簡單的信息,告訴你這次創建是否成功:“yes”或“no”。 如果成 功,本次集成到此爲止;如果失敗,你應該可以很簡單地撤消最後一次的修改,回到前一次成功的創建。在整個

創建過程中,完全不需要你動腦子。

如果有了這樣一套自動化過程,你隨便想多頻繁進行創建都可以。唯一的侷限性就是創建過程本身也會消耗 一定的時間。(譯註:不過與捉蟲所需的時間比起來,這點時間是微不足道的。)

一次成功的創建是什麼樣的?

有 一件重要的事需要確定:怎樣的創建纔算是成功的?看上去很簡單,但是如此簡單的事情有時卻會變得一 團糟,這是值得注意的。有一次,Martin Fowler去檢查一個項目。他問這個項目是否執行日創建,得到了肯定 的回答。幸虧Ron Jeffries也在場,他又提了一個問題:“你們如何處理創建錯誤?”回答是:“我們給相關的 人發一個e-mail。”實際上,這個項目已經好幾個月沒有得到成功的創建了。這不是日創建,這只是日創建的嘗 試。

對於下列“成功創建”的標準,我們還是相當自信的:

  1. 所有最新的源代碼都被配置管理系統驗證合格;
  2. 所有文件都通過重新編譯;
  3. 得到的目標文件(在我們這裏就是Java的class文件)都通過連接,得到可執行文件;
  4. 系統開始運行,針對系統的測試套件(在我們這裏大概有150個測試類)開始運行。

如果所有的步驟都沒有錯誤、沒有人爲干涉,所有的測試也都通過了,我們就得到了一個成功的創建

絕大多數人都認爲“編譯+連接=創建”。至少我們認爲:創建還應該包括啓動應用程序、針對應用程序運行 簡單測試(McConnell稱之爲“冒煙測試”:打開開關讓軟件運行,看它是否會“冒煙”)。

運行更詳盡的測試 集可以大大提高持續集成的價值,所以我們會首選更詳盡的測試。

單一代碼源

爲了實現每日集成,任何開發者都需要能夠很容易地獲取全部最新的源代碼。以前,如果要做一次集成,我 們就必須跑遍整個開發中心,詢問每一個程序員有沒有新的代碼,然後把這些新代碼拷貝過來,再找到合適的插 入位置……沒有什麼比這更糟糕的了。

辦法很簡單。任何人都應該可以帶一臺乾淨的機器過來,連上局域網,然後用一條命令就得到所有的源文 件,馬上開始系統的創建。

最 簡單的解決方案就是:用一套配置管理(源代碼控制)系統作爲所有代碼的來源。配置管理系統通常都設 計有網絡功能,並且帶有讓開發者輕鬆獲取源代碼的工具。而且,它們還提供版本管理工具,這樣你可以很輕鬆 地找到文件以前的版本。成本就更不成問題了,CVS就是一套出色的開放源代碼的配置管理工具。

所有的源文件都應該保存在 配置管理系統中。我說的這個“所有”常常比人們想到的還要多,它還包括創建 腳本、屬性文件、數據庫調度DLL、安裝腳本、以及在一臺乾淨的機器上開始創建所需的其他一切東西。經常都 能看到這樣的情況:代碼得到了控制,但是其他一些重要的文件卻找不到了。

儘量確保所有的東西都保存在配置管理系統的同一 棵代碼源樹中。有時候爲了得到不同的組件,人們會使用 配置管理系統中不同的項目。這帶來的麻煩就是:人們不得不記住哪個組件的哪個版本使用了其他組件的哪些版 本。在某些情況下,你必須將代碼源分開,但是這種情況出現的機率比你想象的要小得多。你可以在從一棵代碼 源樹創建多個組件,上面那些問題可以通過創建腳本來解決,而不必改變存儲結構。

自動化創建腳本

如 果你編寫的是一個小程序,只有十幾個文件,那麼應用程序的創建可能只是一行命令的事:javac *.java。更大的項目就需要更多的創建工作:你可能把文件放在許多目錄裏面,需要確保得到的目標代碼都在適 當的位置;除了編譯,可能還有連接的步驟;你可能還從別的文件中生成了代碼,在編譯之前需要先生成;測試 也需要自動運行。

大 規模的創建經常會耗費一些時間,如果只做了一點小小的改動,當然你不會希望重新做所有這些步驟。所 以好的創建工具會自動分析需要改變的部分,常見的方法就是檢查源文件和目標文件的修改日期,只有當源文件 的修改日期遲於目標文件時,纔會重新編譯。於是,文件之間的依賴就需要一點技巧了:如果一個目標文件發生

了變化,那麼只有那些依賴它的目標文件纔會重新編譯。編譯器可能會處理這類事情,也可能不會。

取決於自己的需要,你可以選擇不同的創建類型:你創建的系統可以有測試代碼,也可以沒有,甚至還可以 選擇不同的測試集;一些組件可以單獨創建。創建腳本應該讓你可以根據不同的情況選擇不同的創建目標。

你輸入一行簡單的命令之後,幫你挑起這副重擔常常是腳本。你使用的可能是shell腳本,也可能是更復雜 的腳本語言(例如Perl或Python)。但是很快你就會發現一個專門設計的創建環境是很有用的,例如Unix下的make工具。

在 我們的Java開發中,我們很快就發現需要一個更復雜的解決方案。Matt用了相當多的時間開發了一個用於 企業級Java開發的創建工具,叫做Jinx。但是,最近我們已經轉而使用開放源代碼的創建工具Ant (http://jakarta.apache.org/ant/index.html)。Ant的設計與Jinx非常相似,也支持Java文件編譯和 Jar封 裝。同時,編寫Ant的擴展也很容易,這讓我們可以在創建過程中完成更多的任務。

許多人都使用IDE,絕大多 數的IDE中都包含了創建管理的功能。但是,這些文件都依賴於特定的IDE,而且 經常比較脆弱,而且還需要在IDE中才能工作。IDE的用戶可以建立自己的項目文件,並且在自己的單獨開發中使 用它們。但是我們的主創建過程用Ant建立,並且在一臺使用Ant的服務器上運行。

自測試的代碼

只讓程序通過編譯還是遠遠不夠的。儘管強類型語言的編譯器可以指出許多問題,但是即使成功通過了編 譯,程序中仍然可能留下很多錯誤。爲了幫助跟蹤這些錯誤,我們非常強調自動化測試——這也是XP提倡的另一 個實踐。

XP 將測試分爲兩類:單元測試和容納測試(也叫功能測試)。單元測試是由開發者自己編寫的,通常只測試 一個類或一小組類。容納測試通常是由客戶或外部的測試組在開發者的幫助下編寫的,對整個系統進行端到端的 測試。這兩種測試我們都會用到,並且儘量提高測試的自動化程度。

作爲創建的一部分,我們需要運行一組被稱爲“BVT” (Build Verification Tests,創建確認測試)的測 試。BVT中所有的測試都必須通過,然後我們才能宣佈得到了一個成功的創建。所有XP風格的單元測試都屬於 BVT。由於本文是關於創建過程的,所以我們所說的“測試”基本上都是指BVT。請記住,除了BVT之外,還有一 條測試線存在(譯註:指功能測試),所以不要把BVT和整體測試、QA等混爲一談。實際上,我們的QA小組根本不會看到沒有通過BVT的代碼,因爲他們只 對成功的創建進行測試。

有一條基本的原則:在編寫代碼的同時,開發者也應該編寫相應的測試。完成任務之後,他們不但要歸 還 (check in)產品代碼,而且還要歸還這些代碼的測試。這也跟XP的“測試第一”的編程風格很相似:在編寫完 相應的測試、並看到測試失敗之前,你不應該編寫任何代碼。所以,如果想給系統添加新特性,你首先應該編寫一個測試。只有當新的特性已經實現了以後,這個測 試纔可能通過。然後,你的工作就是讓這個測試能夠通過。

我們用Java編寫這些測試,與開發使用同樣的語言,所以編寫測 試與編寫代碼沒有太大的區別。我們使用JUnit(http://www.junit.org/)來作爲組織、編寫測試的框架。JUnit是一個簡單的框 架,讓我們可以快速編 寫測試、將測試組織爲套件、並以交互或批處理的模式來運行測試套件。(JUnit是xUnit家族的Java版本 ——xUnit包括了幾乎所有語言的測試框架。)

在編寫軟件的過程中,在每一次的編譯之後,開發者通常都會運行一部分單 元測試。這實際上提高了開發者的工作效率,因爲這些單元測試可以幫助你發現代碼中的邏輯錯誤。然後,你就沒必要去調試查錯,只需要注意 最後一次運行測試之後修改的代碼就行了。這個修改的範圍應該很小,所以尋找bug也就容易多了。

並非所有的人都嚴格遵循 XP“測試第一”的風格,但是在第一時間編寫測試的好處是顯而易見的。它們不但 讓每個人的工作效率更高,而且由這些測試構成的BVT更能捕捉到系統中的錯誤。因爲BVT每天要運行好幾次,所 以BVT檢查出的任何問題都是比較容易改正的,原因很簡單:我們只做了相當小範圍的修改,所以我們可以在這 個範圍內尋找bug。在修改過的一小塊代碼中排錯當然比跟蹤整個系統來排錯要有效多了。

當然,你不能指望測試幫你找到所有的問題。就象人們常說的:測試不能證明系統中不存在錯誤。但是,盡 善盡美不是我們唯一的要求。不夠完美的測試只要經常運行,也比永遠寫不出來的“完美測試”要好得多。

另 一個相關的問題就是:開發者們爲自己的代碼編寫測試。我們經常聽人說:開發者不應該測試自己的代 碼,因爲他們很容易忽視自己工作中的錯誤。儘管這也是事實,但是自測試過程需要快速將測試轉入代碼基礎 中。這種快速轉換的價值超過獨立測試者的價值。所以,我們還是用開發者自己編寫的測試來構造BVT,但是仍 然有獨立編寫的容納測試。

自 測試另一個很重要的部分就是它通過反饋——XP的一項核心價值——來提高測試的質量。這裏的反饋來自 於從BVT中逃脫的bug。自測試的規則是:除非你在BVT中加入了相應的測試,否則就不能修正任何錯誤。這樣, 每當要修正某個錯誤的時候,你都必須添加相應的測試,以確保BVT不會再把錯誤放過去。而且,這個測試應該 引導你去考慮更多的測試、編寫更多的測試來加強BVT。

主創建

創建過程的自動化對於單個開發者來說很有意義,但是它真正發光的,還是在整個系統的主創建(master build)的生成。我們發現,主創建過程能讓整個團隊走到一起來,讓他們及早發現集成中的問題。

第一步是要選擇運行主創建的機器。我們選擇了一臺叫做“投石車”的計算機(我們經常玩“帝國時代” J),這是一臺裝有四個CPU的服務器,非常適合專門用來做創建。(由於完整的創建需要相當長的時間,所以這 種馬力是必須的。)

創建進程是在一個隨時保持運行的Java類中進行的。如果沒有創建任務,創建進程就一直循環等待,每過幾 分鐘去檢查一下代碼倉庫。如果在最後的創建之後沒有人歸還任何代碼,進程就繼續等待。如果代碼倉庫中有了 新的代碼,就開始創建。

創 建的第一階段是完全提取倉庫中的代碼。Starteam已經爲我們提供了相當好的Java API,所以切入代碼倉 庫也很容易。守護進程(daemon)會觀察五分鐘以前的倉庫,看最近五分鐘裏面有沒有人歸還了代碼。如果有, 守護進程就會考慮等五分鐘再提取代碼(以免在別人歸還代碼的過程中提取)。

守護進程將全部代碼提取到投石機的一個目錄 中。提取完成之後,守護進程就會在這個目錄裏調用Ant腳 本。然後,Ant會接管整個創建過程,對所有源代碼做一次完整的創建。Ant腳本會負責整個編譯過程,並把得到 的class文件放進六個jar包裏,發佈到EJB服務器上。

當Ant完成了編譯和發佈的工作之後,創建守護進程就會在 EJB服務器上開始運行新的jar,同時開始運行BVT 測試套件。如果所有的測試都能正常運行通過,我們就得到了一個成功的創建。然後創建守護進程就會回到 Starteam,將所有提取出的源代碼標記上創建號。然後,守護進程會觀察創建過程中是否還有人歸還了代碼。如 果有,就再開始一次創建;如果沒有,守護進程就回到它的循環中,等待下一次的歸還。

創建結束之後,創建守護進程會給所有向最新一次創建歸還了代碼的開發者發一個e-mail,彙報創建的情 況。如果把創建留在代碼歸還之後去做,而又不用e-mail向開發者通報創建的情況,我們通常認爲這是不好的組 織形式。

守護進程將所有的步驟都寫在XML格式的日誌文件裏面。投石車上會運行一個servlet,允許任何人通過它檢 查日誌,以觀察創建的狀態。(見圖1)

屏幕上會顯示出創建是否正在運行、開始運行的時間。在左邊有所有創建的歷史記錄,成功的、失敗的都記 錄在案。點擊其中的某一條記錄,就會顯示出這次創建的詳細信息:編譯是否通過、測試的結果、發生了哪些變 化……

我們發現很多開發者都經常看看這個頁面,因爲它讓他們看到項目發展的方向,看到隨着人們不斷歸還代碼 而發生的變化。有時我們也會在這個頁面上放一些其他的項目新聞,但是需要把握好尺度。

要讓開發者能在自己的本地機器上模擬主創建過程,這是很重要的。這樣,如果集成錯誤出現了,開發者可 以在自己的機器上研究、調試,而不必真的執行主創建過程。而且,開發者也可以在歸還代碼之前先在本地執行 創建,從而降低了主創建失敗的可能性。

這 裏有一個比較重要的問題:主創建應該是乾淨的創建(完全從源代碼開始)還是增量創建?增量創建會快 得多,但是也增大了引入錯誤的風險,因爲有些部分是沒有編譯的。而且我們還有無法重新創建的風險。我們的 創建速度相當快(20萬行代碼約15分鐘),所以我們樂於每次都做乾淨的創建。但是,有些團隊喜歡在大多數時候做增量創建,但是當那些奇怪的問題突然出現 時,也經常性地做乾淨的創建(至少每天一次)。

圖1:運行在投石車上的servlet

代碼歸還(Check in)

使 用自動化創建就意味着開發者應該遵循某種節奏來開發軟件,最重要的就是他們應該經常集成。我們曾經 見過一些組織,他們也做日創建,但是其中的開發者卻不經常歸還代碼。如果開發者幾周才歸還一次代碼,那麼 日創建又有什麼意義呢?我們遵循的原則是:每個開發者至少每天要歸還一次代碼。

在開始新的任務之前,開發者應該首先與配置管理系統同步。也就是說,他們應該首先更新本地機器上的源 代碼。在舊的代碼基礎上編寫代碼,這隻會帶來麻煩和混亂。

然後,開發者要隨時保持文件的更新。開發者可以在一段任務完成之後將代碼集成到整個系統中,也可以在 任務的中途集成,但是在集成的時候必須保證所有的測試都能通過。

集成的第一步是要再次使開發者的本地文件與代碼倉庫同步。代碼倉庫中所有新近有改動的文件都要拷貝到 開發者的工作目錄中來,當文件發生衝突時,配置管理系統會向開發者提出警告。然後,開發者需要對同步後的 工作集進行創建,對這些文件運行BVT,並得到正確的結果。

現 在,開發者可以把新的文件提交到代碼倉庫中。提交完成之後,開發者就需要等待主創建。如果主創建成功,那麼這次歸還也是成功的。如果主創建失敗了,開發者 可以在本地修改。如果修改很簡單,就可以直接提 交;如果修改比較複雜,開發者就需要放棄這次修改,重新同步自己的工作目錄,然後繼續在本地開發、調試,然後再次提交。

某 些系統強制要求歸還進程逐個進行。在這種情況下,系統中會有一個創建令牌,同一時間只有一個開發者 能拿到令牌。開發者獲取創建令牌,再次同步文件,提交修改,然後釋放令牌。這就確保創建過程中,最多隻能 有一個開發者在更新代碼倉庫。不過我們發現,即使沒有創建令牌,我們也很少遇到麻煩,所以我們也不用這種方法。經常會有多個人同時向同一個主創建提交代碼 的情況,但是這很少造成創建失敗,而且這樣的錯誤也很容 易修復。

同時,我們還讓開發者自己來決定歸還過程中的小心程 度。這反映出開發者對集成錯誤出現機率的評估。如 果她覺得很有可能出現集成錯誤,那麼她就會在歸還之前先做一次本地創建;如果她覺得根本不可能出現集成錯誤,那麼她可以直接歸還。如果犯了錯誤,在主創建 運行時她立刻就會發現,然後她就必須放棄自己的修改,找到出錯的地方。如果錯誤很容易發現、很容易修補,那麼這種錯誤也是可以接受的。

總結

發展一個制度嚴密的自動化創建過程對於項目的控制是很重要的。許多軟件先賢都這樣說,但是我們發現, 這樣的過程在軟件開發領域中仍然罕見。

關鍵是要讓所有的事情都完全自動化,並且要經常進行集成,這樣才能儘快發現錯誤。然後,人們可以隨時 修改需要修改的東西,因爲他們知道:如果他們做的修改引起了集成錯誤,那也是很容易發現和修補的。一旦獲得了這些利益,你會發現自己再也無法放下它們。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章