譯者:雷鎮(zhèn)
最后更改于:2006年5月1日
目錄
用持續(xù)集成構(gòu)建特性
持續(xù)集成實踐
只維護一個源碼倉庫
自動化 build
讓你的build自行測試
每人每天都要向mainline提交代碼
每次提交都應在集成計算機上重新構(gòu)建 mainline
保持快速 build
在模擬生產(chǎn)環(huán)境中進行測試
讓每個人都能輕易獲得最新的可執(zhí)行文件
每個人都能看到進度
自動化部署
持續(xù)集成的益處
引入持續(xù)集成
最后的思考
延伸閱讀
我還可以生動記起第一次看到大型軟件工程的情景。我當時在一家大型英國電子公司的QA部門實習。我的經(jīng)理帶我熟悉公司環(huán)境,我們進到一間巨大的,充滿了壓抑感和格子間的的倉庫。我被告知這個項目已經(jīng)開發(fā)了好幾年,現(xiàn)在正在集成階段,并已經(jīng)集成了好幾個月。我的向?qū)н€告訴我沒人知道集成要多久才能結(jié)束。從此我學到了軟件開發(fā)的一個慣例:集成是一個很耗時并難以預測的過程。但是事實并非總是如此,我的 ThoughWorks 同事所做的項目,以及很多其它遍布世界各地的軟件項目,都不會把集成當回事。任何一個開發(fā)者本地的代碼和項目共享基準代碼的差別僅僅只有幾小時的工作而已,而且這只要幾分鐘的時間就可以被集成回去。任何集成錯誤都可以很快被發(fā)現(xiàn),并被快速修復。這鮮明的差別并非源于昂貴和復雜的工具。其中的精華蘊含于一個簡單的實踐:使用統(tǒng)一的代碼倉庫并頻繁集成(通常每天一次)。
當我向別人介紹持續(xù)集成方法時,人們通常會有兩種反應:“這(在我們這兒)不管用”和“做了也不可能有什么不同”。但如果他們真的試過了,就會發(fā)現(xiàn)持續(xù)集成其實比聽起來要簡單,并且能給開發(fā)過程帶來巨大的改變。因此第三種常見的反應是:“我們就是這么做的,做開發(fā)怎可能不用它呢?”
“持續(xù)集成”一詞來源于極限編程(Extreme Programming),作為它的12個實踐之一出現(xiàn)。當我開始在 ThoughWorks 開始顧問職業(yè)生涯時,我鼓勵我所參與的項目使用這種技巧。Matthew Foemmel 將我抽象的指導思想轉(zhuǎn)化為具體的行動。我們看到了項目從少而繁雜的集成進步到我所描述的不當回事。Metthew和我將我們的經(jīng)驗寫進了這篇論文的第一版里。這篇論文后來成了我網(wǎng)站里最受歡迎的文章之一。
盡管持續(xù)集成不需要什么特別的工具,我們發(fā)現(xiàn)使用一個持續(xù)集成服務(wù)器軟件還是很有效果的。最出名的持續(xù)集成服務(wù)器軟件是 CruiseControl,這是一個開源工具,最早由 ThoughWorks 的幾個人開發(fā),現(xiàn)在由社區(qū)維護。之后還有許多其他持續(xù)集成服務(wù)器軟件出現(xiàn),有些是開源的,有些則是商業(yè)軟件,比如 ThoughtWorks Studio 的 Cruise。
對我來說,解釋持續(xù)集成最簡單的方法就是用一個簡單的例子來示范開發(fā)一個小 feature?,F(xiàn)在假設(shè)我要完成一個軟件的一部分功能,具體任務(wù)是什么并不重要,我們先假設(shè)這個 feature 很小,只用幾個小時就可以完成。(我們稍后會研究更大的任務(wù)的情況。)
一開始,我將已集成的源代碼復制一份到本地計算機。這可以通過從源碼管理系統(tǒng)的 mainline 上 check out 一份源代碼做到。
如果你用過任何源代碼管理系統(tǒng),理解上面的文字應該不成問題。但如果你沒用過,可能會有讀天書的感覺。所以我們先快速解釋一個這些概念。源代碼管理系統(tǒng)將項目的所有源代碼都保存在一個“倉庫(repository)”中。系統(tǒng)的當前狀態(tài)通常被稱為“mainline”。開發(fā)者隨時都可以把mainline復制一份到他們自己的計算機,這個過程被稱為“check out”。開發(fā)者計算機上的拷貝被稱為“工作拷貝(working copy)”。(絕大部分情況下,你最終都會把工作拷貝的內(nèi)容提交到mainline上去,所以兩者實際上應該差不多。)
現(xiàn)在我拿到了工作拷貝,接下來需要做一些事情來完成任務(wù)。這包括修改產(chǎn)品代碼和添加修改自動化測試。在持續(xù)集成中,軟件應該包含完善的可自動運行的測試,我稱之為自測試代碼。這一般需要用到某一個流行的 XUnit 測試框架。
一旦完成了修改,我就會在自己的計算機上啟動一個自動化 build。這會將我的工作拷貝中的源代碼編譯并鏈接成為一個可執(zhí)行文件,并在之上運行自動化測試。只有當所有的 build 和測試都完成并沒有任何錯誤時,這個 build 過程才可以認為是成功的。
當我 build 成功后,我就可以考慮將改動提交到源碼倉庫。但麻煩的情況在于別人可能已經(jīng)在我之前修改過 mainline。這時我需要首先把別人的修改更新到我的工作拷貝中,再重新做 build。如果別人的代碼和我的有沖突,就會在編譯或測試的過程中引起錯誤。我有責任改正這些問題,并重復這一過程,直到我的工作拷貝能通過 build 并和 mainline 的代碼同步。
一旦我本地的代碼能通過 build,并和 mainline 同步,我就可以把我的修改提交到源碼倉庫。
然而,提交完代碼不表示就完事大吉了。我們還要做一遍集成 build,這次在集成計算機上并要基于 mainline 的代碼。只有這次 build 成功了,我的修改才算告一段落。因為總有可能我忘了什么東西在自己的機器上而沒有更新到源碼倉庫。只有我提交的改動被成功的集成了,我的工作才能結(jié)束。這可以由我手工運行,也可以由 Cruise 自動運行。
如果兩個開發(fā)者的修改存在沖突,這通常會被第二個人提交代碼前本地做 build 時發(fā)現(xiàn)。即使這時僥幸過關(guān),接下來的集成 build 也會失敗掉。不管怎樣,錯誤都會被很快檢測出來。此時首要的任務(wù)就是改正錯誤并讓 build 恢復正常。在持續(xù)集成環(huán)境里,你必須盡可能快地修復每一個集成 build。好的團隊應該每天都有多個成功的 build。錯誤的 build 可以出現(xiàn),但必須盡快得到修復。
這樣做的結(jié)果是你總能得到一個穩(wěn)定的軟件,它可能有一些 bug,但可以正常工作。每個人都基于相同的穩(wěn)定代碼進行開發(fā),而且不會離得太遠,否則就會不得不花很長時間集成回去。Bug被發(fā)現(xiàn)得越快,花在改正上的時間就越短。
持續(xù)集成實踐
從上面的故事我們大概了解了持續(xù)集成是如何在我們的日常工作中發(fā)揮作用的。但讓一切正常運行起來還需要掌握更多的知識。我接下來會集中講解一下高效持續(xù)集成的關(guān)鍵實踐。
在軟件項目里需要很多文件協(xié)調(diào)一致才能 build 出產(chǎn)品。跟蹤所有這些文件是一項困難的工作,尤其是當有很多人一起工作時。所以,一點也不奇怪,軟件開發(fā)者們這些年一直在研發(fā)這方面的工具。這些工具稱為源代碼管理工具,或配置管理,或版本管理系統(tǒng),或源碼倉庫,或各種其它名字。大部分開發(fā)項目中它們是不可分割的一部分。但可惜的是,并非所有項目都是如此。雖然很罕見,但我確實參加過一些項目,它們直接把代碼存到本地驅(qū)動器和共享目錄中,亂得一塌糊涂。
所以,作為一個最基本的要求,你必須有一個起碼的源代碼管理系統(tǒng)。成本不會是問題,因為有很多優(yōu)秀的開源工具可用。當前較好的開源工具是 Subversion。(更老的同樣開源的 CVS 仍被廣泛使用,即使是 CVS 也比什么都不用強得多,但 Subversion 更先進也更強大。)有趣的是,我從與開發(fā)者們的交談中了解到,很多商業(yè)源代碼管理工具其實不比 Subversion 更好。只有一個商業(yè)軟件是大家一致同意值得花錢的,這就是 Perforce。
一旦你有了源代碼管理系統(tǒng),你要確保所有人都知道到哪里去取代碼。不應出現(xiàn)這樣的問題:“我應該到哪里去找xxx文件?” 所有東西都應該存在源碼倉庫里。
即便對于用了源碼倉庫的團隊,我還是觀察到一個很普遍的錯誤,就是他們沒有把所有東西都放在源碼倉庫里。一般人們都會把代碼放進去,但還有許多其它文件,包括測試腳本,配置文件,數(shù)據(jù)庫Schema,安裝腳本,還有第三方的庫,所有這些build時需要的文件都應該放在源碼倉庫里。我知道一些項目甚至把編譯器也放到源碼倉庫里(用來對付早年間那些莫名其妙的C++編譯器很有效)。一個基本原則是:你必須能夠在一臺干凈的計算機上重做所有過程,包括checkout和完全build。只有極少量的軟件需要被預裝在這臺干凈機器上,通常是那些又大又穩(wěn)定,安裝起來很復雜的軟件,比如操作系統(tǒng),Java開發(fā)環(huán)境,或數(shù)據(jù)庫系統(tǒng)。
你必須把build需要的所有文件都放進源代碼管理系統(tǒng),此外還要把人們工作需要的其他東西也放進去。IDE配置文件就很適合放進去,因為大家共享同樣的IDE配置可以讓工作更簡單。
版本控制系統(tǒng)的主要功能之一就是創(chuàng)建 branch 以管理開發(fā)流。這是個很有用的功能,甚至可以說是一個基礎(chǔ)特性,但它卻經(jīng)常被濫用。你最好還是盡量少用 branch。一般有一個mainline就夠了,這是一條能反映項目當前開發(fā)狀況的 branch。大部分情況下,大家都應該從mainline出發(fā)開始自己的工作。(合理的創(chuàng)建 branch 的理由主要包括給已發(fā)布的產(chǎn)品做維護和臨時性的實驗。)
一般來說,你要把build依賴的所有文件放進代碼管理系統(tǒng)中,但不要放build的結(jié)果。有些人習慣把最終產(chǎn)品也都放進代碼管理系統(tǒng)中,我認為這是一種壞味道——這意味著可能有一些深層次的問題,很可能是無法可靠地重新build一個產(chǎn)品。
通常來說,由源代碼轉(zhuǎn)變成一個可運行的系統(tǒng)是一個復雜的過程,牽扯到編譯,移動文件,將 schema 裝載到數(shù)據(jù)庫,諸如此類。但是,同軟件開發(fā)中的其它類似任務(wù)一樣,這也可以被自動化,也必須被自動化。要人工來鍵入各種奇怪的命令和點擊各種對話框純粹是浪費時間,也容易滋生錯誤。
在大部分開發(fā)平臺上都能找到自動化 build 環(huán)境的影子。比如 make,這在 Unix 社區(qū)已經(jīng)用了幾十年了,Java 社區(qū)也開發(fā)出了 Ant,.NET 社區(qū)以前用 Nant,現(xiàn)在用 MSBuild。不管你在什么平臺上,都要確保只用一條命令就可以運行這些腳本,從而 build 并運行系統(tǒng)。
一個常見的錯誤是沒有把所有事都放進自動化 build。比如:Build 也應該包括從源碼倉庫中取出數(shù)據(jù)庫 schema 并在執(zhí)行環(huán)境中設(shè)置的過程。我要重申一下前面說過的原則:任何人都應該能從一個干凈的計算機上 check out 源代碼,然后敲入一條命令,就可以得到能在這臺機器上運行的系統(tǒng)。
Build 腳本有很多不同的選擇,依它們所屬的平臺和社區(qū)而定,但也沒有什么定勢。盡管大部分的 Java 項目都用 Ant,還是有一些項目用 Ruby(Ruby Rake 是一個不錯的 build 腳本工具)。我們也曾經(jīng)用 Ant 自動化早期的 Microsoft COM 項目,事實證明很有價值。
一個大型 build 通常會很耗時,如果只做了很小的修改,你不會想花時間去重復所有的步驟。所以一個好的 build 工具應該會分析哪些步驟可以跳過。一個通用的辦法是比較源文件和目標文件的修改時間,并只編譯那些較新的源文件。處理依賴關(guān)系要麻煩一些:如果一個目標文件修改了,所有依賴它的部分都要重新生成。編譯器可能會幫你處理這些事情,也可能不會。
根據(jù)你的需要,你可能會想 build 出各種不同的東西。你可以同時 build 系統(tǒng)代碼和測試代碼,也可以只 build 系統(tǒng)代碼。一些組件可以被單獨 build。Build 腳本應該允許你在不同的情況中 build 不同的 target。
我們許多人都用 IDE,許多 IDE 都內(nèi)置包含某種 build 管理功能。然而,相應的配置文件往往是這些 IDE 的專有格式,而且往往不夠健壯,它們離了 IDE 就無法工作。如果只是 IDE 用戶自己一個人開發(fā)的話,這還能夠接受。但在團隊里,一個工作于服務(wù)器上的主 build 環(huán)境和從其它腳本里運行的能力更重要。我們認為,在 Java 項目里,開發(fā)者可以用自己的 IDE 做 build,但主 build 必須用 Ant 來做,以保證它可以在開發(fā)服務(wù)器上運行。
傳統(tǒng)意義上的 build 指編譯,鏈接,和一些其它能讓程序運行起來的步驟。程序可以運行并不意味著它也工作正?!,F(xiàn)代靜態(tài)語言可以在編譯時檢測出許多 bug,但還是有更多的漏網(wǎng)之魚。
一種又快又省的查 bug 的方法是在 build 過程中包含自動測試。當然,測試并非完美解決方案,但它確實能抓住很多 bug——多到可以讓軟件真正可用。極限編程(XP)和測試驅(qū)動開發(fā)(TDD)的出現(xiàn)很好地普及了自測試代碼的概念,現(xiàn)在已經(jīng)有很多人意識到了這種技巧的價值。
經(jīng)常讀我的著作的讀者都知道我是 TDD 和 XP 的堅定追隨者。但是我想要強調(diào)你不需要這兩者中任何一個就能享受自測試代碼的好處。兩者都要求你先寫測試,再寫代碼以通過測試,在這種工作模式里測試更多著重于探索設(shè)計而不是發(fā)現(xiàn) bug。這絕對是一個好方法,但對于持續(xù)集成而言它并不必要,因為這里對自測試代碼的要求沒有那么高。(盡管我肯定會選擇用 TDD 的方式。)
自測試代碼需要包含一套自動化測試用例,這些測試用例可以檢查大部分代碼并找出 bug。測試要能夠從一條簡單的命令啟動。測試結(jié)果必須能指出哪些測試失敗了。對于包含測試的 build,測試失敗必須導致 build 也失敗。
在過去的幾年里,TDD 的崛起普及了開源的 XUnit 系列工具,這些工具用作以上用途非常理想。對于我們在 ThoughWorks 工作的人來說,XUnit 工具已經(jīng)證明了它們的價值。我總是建議人們使用它們。這些最早由 Kent Beck 發(fā)明的工具使得設(shè)置一個完全自測試環(huán)境的工作變得非常簡單。
毋庸置疑,對于自動測試的工作而言,XUnit 工具只是一個起點。你還必須自己尋找其他更適合端對端測試的工具?,F(xiàn)在有很多此類工具,包括FIT,Selenium,Sahi,Watir,FITnesse,和許多其它我無法列在這里的工具。
當然你不能指望測試發(fā)現(xiàn)所有問題。就像人們經(jīng)常說的:測試通過不能證明沒有 bug。然而,完美并非是你要通過自測試 build 達到的唯一目標。經(jīng)常運行不完美的測試要遠遠好過夢想著完美的測試,但實際什么也不做。
集成的主要工作其實是溝通。集成可以讓開發(fā)者告訴其他人他們都改了什么東西。頻繁的溝通可以讓人們更快地了解變化。
讓開發(fā)者提交到 mainline 的一個先決條件是他們必須能夠正確地 build 他們的代碼。這當然也包括通過 build 包含的測試。在每個提交迭代里,開發(fā)者首先更新他們的工作拷貝以與 mainline 一致,解決任何可能的沖突,然后在自己的機器上做 build。在 build 通過后,他們就可以隨便向 mainline 提交了。
通過頻繁重復上述過程,開發(fā)者可以發(fā)現(xiàn)兩個人之間的代碼沖突。解決問題的關(guān)鍵是盡早發(fā)現(xiàn)問題。如果開發(fā)者每過幾個小時就會提交一次,那沖突也會在出現(xiàn)的幾個小時之內(nèi)被發(fā)現(xiàn),從這一點來說,因為還沒有做太多事,解決起來也容易。如果讓沖突待上幾個星期,它就會變得非常難解決。
因為你在更新工作拷貝時也會做 build,這意味著你除了解決源代碼沖突外也會檢查編譯沖突。因為 build 是自測試的,你也可以查出代碼運行時的沖突。后者如果在一段較長的時間還沒被查出的話會變得尤其麻煩。因為兩次提交之間只有幾個小時的修改,產(chǎn)生這些問題只可能在很有限的幾個地方。此外,因為沒改太多東西,你還可以用 diff-debugging 的技巧來找 bug。
總的來說,我的原則是每個開發(fā)者每天都必須提交代碼。實踐中,如果開發(fā)者提交的更為頻繁效果也會更好。你提交的越多,你需要查找沖突錯誤的地方就越少,改起來也越快。
頻繁提交客觀上會鼓勵開發(fā)者將工作分解成以小時計的小塊。這可以幫助跟蹤進度和讓大家感受到進展。經(jīng)常會有人一開始根本無法找到可以在幾小時內(nèi)完成的像樣的工作,但我們發(fā)現(xiàn)輔導和練習可以幫助他們學習其中的技巧。
使用每日提交的策略后,團隊就能得到很多經(jīng)過測試的 build。這應該意味著 mainline 應該總是處于一種健康的狀態(tài)。但在實踐中,事情并非總是如此。一個原因跟紀律有關(guān),人們沒有嚴格遵守在提交之前在本地更新并做 build 的要求。另一個原因是開發(fā)者的計算機之間環(huán)境配置的不同。
結(jié)論是你必須保證日常的 build 發(fā)生在專用的集成計算機上,只有集成 build 成功了,提交的過程才算結(jié)束。本著“誰提交,誰負責”的原則,開發(fā)者必須監(jiān)視 mainline 上的 build 以便失敗時及時修復。一個推論是如果你在下班前提交了代碼,那你在 mainline build 成功之前就不能回家。
我知道主要有兩種方法可以使用:手動 build,或持續(xù)集成服務(wù)器軟件。
手動 build 描述起來比較簡單。基本上它跟提交代碼之前在本地所做的那次 build 差不多。開發(fā)者登錄到集成計算機,check out 出 mainline 上最新的源碼(已包含最新的提交),并啟動一個集成 build。他要留意 build 的進程,只有 build 成功了他的提交才算成功。(請查看 Jim Shore 的描述。)
持續(xù)集成服務(wù)器軟件就像一個監(jiān)視著源碼倉庫的監(jiān)視器。每次源碼倉庫中有新的提交,服務(wù)器就會自動 check out 出源代碼并啟動一次 build,并且把 build 的結(jié)果通知提交者。這種情況下,提交者的工作直到收到通知(通常是 email)才算結(jié)束。
在 ThoughtWorks,我們都是持續(xù)集成服務(wù)器軟件的堅定支持者,實際上我們引領(lǐng)了 CruiseControl 和 CruiseControl.NET 最早期的開發(fā),兩者都是被廣泛使用的開源軟件。此后,我們還做了商業(yè)版的 Cruise 持續(xù)集成服務(wù)器。我們幾乎在每一個項目里都會用持續(xù)集成服務(wù)器,并且對結(jié)果非常滿意。
不是每個人都會用持續(xù)集成服務(wù)器。Jim Shore 就清楚地表達了為什么他更偏好手動的辦法。我同意他的看法中的持續(xù)集成并不僅僅是安裝幾個軟件而已,所有的實踐都必須為了能讓持續(xù)集成更有效率。但同樣的,許多持續(xù)集成執(zhí)行得很好的團隊也會發(fā)現(xiàn)持續(xù)集成服務(wù)器是個很有用的工具。
許多組織根據(jù)安排好的日程表做例行 build,如每天晚上。這其實跟持續(xù)集成是兩碼事,而且做得遠遠不夠。持續(xù)集成的最終目標就是要盡可能快地發(fā)現(xiàn)問題。Nightly build 意味著 bug 被發(fā)現(xiàn)之前可能會待上整整一天。一旦 bug 能在系統(tǒng)里呆這么久,找到并修復它們也會花較長的時間。
做好持續(xù)集成的一個關(guān)鍵因素是一旦 mainline 上的 build 失敗了,它必須被馬上修復。而在持續(xù)集成環(huán)境中工作最大的好處是,你總能在一個穩(wěn)定的基礎(chǔ)上做開發(fā)。mainline 上 build 失敗并不總是壞事,但如果它經(jīng)常出錯,就意味著人們沒有認真地在提交代碼前先在本地更新代碼和做 build。當 mainline 上 build 真的失敗時,第一時間修復就成了頭等大事。為了防止在 mainline 上的問題,你也可以考慮用 pending head 的方法。
當團隊引入持續(xù)集成時,這通常是最難搞定的事情之一。在初期,團隊會非常難以接受頻繁在 mainline 上做 build 的習慣,特別當他們工作在一個已存在的代碼基礎(chǔ)上時更是如此。但最后耐心和堅定不移的實踐常常會起作用,所以不要氣餒。
持續(xù)集成的重點就是快速反饋。沒有什么比緩慢的 build 更能危害持續(xù)集成活動。這里我必須承認一個奇思怪想的老家伙關(guān)于 build 快慢標準的的玩笑(譯者注:原文如此,不知作者所指)。我的大部分同事認為超過1小時的 build 是不能忍受的。團隊們都夢想著把 build 搞得飛快,但有時我們也確實會發(fā)現(xiàn)很難讓它達到理想的速度。
對大多數(shù)項目來說,XP 的10分鐘 build 的指導方針非常合理。我們現(xiàn)在做的大多數(shù)項目都能達到這個要求。這值得花些力氣去做,因為你在這里省下的每一分鐘都能體現(xiàn)在每個開發(fā)者每次提交的時候。持續(xù)集成要求頻繁提交,所以這積累下來能節(jié)省很多時間。如果你一開始就要花1小時的時間做 build,想加快這個過程會相當有挑戰(zhàn)。即使在一個從頭開始的新項目里,想讓 build 始終保持快速也是很有挑戰(zhàn)的。至少在企業(yè)應用里,我們發(fā)現(xiàn)常見的瓶頸出現(xiàn)在測試時,尤其當測試涉及到外部服務(wù)如數(shù)據(jù)庫。
也許最關(guān)鍵的一步是開始使用分階段build(staged build)。分階段 build(也被稱作 build 生產(chǎn)線)的基本想法是多個 build 按一定順序執(zhí)行。向 mainline 提交代碼會引發(fā)第一個 build,我稱之為提交 build(commit build)。提交 build 是當有人向 mainline 提交時引發(fā)的 build。提交 build 要足夠快,因此它會跳過一些步驟,檢測 bug 的能力也較弱。提交 build 是為了平衡質(zhì)量檢測和速度,因此一個好的提交 build 至少也要足夠穩(wěn)定以供他人基于此工作。
一旦提交 build 成功,其他人就可以放心地基于這些代碼工作了。但別忘了你還有更多更慢的測試要做,可以另找一臺計算機來運行運行這些測試。
一個簡單的例子是兩階段 build。第一階段會編譯和運行一些本地測試,與數(shù)據(jù)庫相關(guān)的單元測試會被完全隔離掉(stub out)。這些測試可以運行得非??欤衔覀兊?0分鐘指導方針。但是所有跟大規(guī)模交互,尤其是真正的數(shù)據(jù)庫交互的 bug 都無法被發(fā)現(xiàn)。第二階段的 build 運行一組不同的測試,這些測試會調(diào)用真正的數(shù)據(jù)庫并涉及更多的端到端的行為。這些測試會跑上好幾小時。
這種情況下,人們用第一階段作為提交 build,并把這作為主要的持續(xù)集成工作。第二階段 build 是次級build,只有在需要的時候才運行,從最后一次成功的提交 build 中取出可執(zhí)行文件作進一步測試。如果次級 build 失敗了,大家不會立刻停下手中所有工作去修復,但團隊也要在保證提交 build 正常運行的同時盡快修正 bug。實際上次級 build 并非一定要正常運行,只要 bug 都能夠被檢查出來并且能盡快得到解決就好。在兩階段 build 的例子里,次級 build 經(jīng)常只是純粹的測試,因為通常只是測試拖慢了速度。
如果次級 build 檢查到了 bug,這是一個信號,意味著提交 build 需要添加一個新測試了。你應該盡可能把次級 build 失敗過的測試用例都添加到提交 build 中,使得提交 build 有能力驗證這些 bug。每當有 bug 繞過提交測試,提交測試總能通過這種方法被加強。有時候確實無法找到測試速度和 bug 驗證兼顧的方法,你不得不決定把這個測試放回到次級 build 里。但大部分情況下都應該可以找到合適加入提交 build 的測試。
上面這個例子是關(guān)于兩階段 build,但基本原則可以被推廣到任意數(shù)量的后階段 build。提交 build 之后的其它 build 都可以同時進行,所以如果你的次級測試要兩小時才能完成,你可以通過用兩臺機器各運行一半測試來快一點拿到結(jié)果。通過這個并行次級 build 技巧,你可以向日常 build 流程中引入包括性能測試在內(nèi)的各種自動化測試。(當我過去幾年內(nèi)參加 Thoughtworks 的各種項目時,我碰到了很多有趣的技巧,我希望能夠說服一些開發(fā)者把這些經(jīng)驗寫出來。)
測試的關(guān)鍵在于在受控條件下找出系統(tǒng)內(nèi)可能在實際生產(chǎn)中出現(xiàn)的任何問題。這里一個明顯的因素是生產(chǎn)系統(tǒng)的運行環(huán)境。如果你不在生產(chǎn)環(huán)境做測試,所有環(huán)境差異都是風險,可能最終造成測試環(huán)境中運行正常的軟件在生產(chǎn)環(huán)境中無法正常運行。
自然你會想到建立一個與生產(chǎn)環(huán)境盡可能完全相同的測試環(huán)境。用相同的數(shù)據(jù)庫軟件,還要同一個版本;用相同版本的操作系統(tǒng);把所有生產(chǎn)環(huán)境用到的庫文件都放進測試環(huán)境中,即使你的系統(tǒng)沒有真正用到它們;使用相同的IP地址和端口;以及相同的硬件;
好吧,現(xiàn)實中還是有很多限制的。如果你在寫一個桌面應用軟件,想要模擬所有型號的裝有不同第三方軟件的臺式機來測試顯然是不現(xiàn)實的。類似的,有些生產(chǎn)環(huán)境可能因為過于昂貴而無法復制(盡管我常碰到出于經(jīng)濟考慮拒絕復制不算太貴的環(huán)境,結(jié)果得不償失的例子)。即使有這些限制,你的目標仍然是盡可能地復制生產(chǎn)環(huán)境,并且要理解并接受因測試環(huán)境和生產(chǎn)環(huán)境不同帶來的風險。
如果你的安裝步驟足夠簡單,無需太多交互,你也許能在一個模擬生產(chǎn)環(huán)境里運行提交 build。但事實上系統(tǒng)經(jīng)常反應緩慢或不夠穩(wěn)定,這可以用 test double 來解決。結(jié)果常常是提交測試為了速度原因在一個假環(huán)境內(nèi)運行,而次級測試運行在模擬真實的生產(chǎn)環(huán)境中。
我注意到越來越多人用虛擬化來搭建測試環(huán)境。虛擬機的狀態(tài)可以被保存,因此安裝并測試最新版本的build相對簡單。此外,這可以讓你在一臺機器上運行多個測試,或在一臺機器上模擬網(wǎng)絡(luò)里的多臺主機。隨著虛擬化性能的提升,這種選擇看起來越來越可行。
軟件開發(fā)中最困難的部分是確定你的軟件行為符合預期。我們發(fā)現(xiàn)事先清楚并正確描述需求非常困難。對人們而言,在一個有缺陷的東西上指出需要修改的地方要容易得多。敏捷開發(fā)過程認可這種行為,并從中受益。
為了以這種方式工作,項目中的每個人都應該能拿到最新的可執(zhí)行文件并運行。目的可以為了 demo,也可以為了探索性測試,或者只是為了看看這周有什么進展。
這做起來其實相當簡單:只要找到一個大家都知道的地方來放置可執(zhí)行文件即可??梢酝瑫r保存多份可執(zhí)行文件以備使用。每次放進去的可執(zhí)行文件應該要通過提交測試,提交測試越健壯,可執(zhí)行文件就會越穩(wěn)定。
如果你采用的過程是一個足夠好的迭代過程,把每次迭代中最后一個 build 放進去通常是明智的決定。Demo 是一個特例,被 demo 的軟件特性都應該是演示者熟悉的特性。為了 demo 的效果值得犧牲掉最新的 build,轉(zhuǎn)而找一個早一點但演示者更熟悉的版本。
持續(xù)集成中最重要的是溝通。你需要保證每個人都能輕易看到系統(tǒng)的狀態(tài)和最新的修改。
溝通的最重要的途徑之一是 mainline build。如果你用 Cruise,一個內(nèi)建的網(wǎng)站會告訴你是否正有 build 在進行,和最近一次 mainline build 的狀態(tài)。許多團隊喜歡把一個持續(xù)工作的狀態(tài)顯示設(shè)備連接到 build 系統(tǒng)來讓這個過程更加引人注目,最受歡迎的顯示設(shè)備是燈光,綠燈閃亮表示 build 成功,紅燈表示失敗。一種常見的選擇是紅色和綠色的熔巖燈,這不僅僅指示 build 的狀態(tài),還能指示它停留在這個狀態(tài)的時間長短,紅燈里出現(xiàn)氣泡表示 build 出問題已經(jīng)太長時間了。每一個團隊都會選擇他們自己的 build 傳感器。如果你的選擇帶點幽默性和娛樂性效果會更好(最近我看到有人在實驗跳舞兔)。
即使你在使用手動持續(xù)集成,可見程度依然很重要。Build 計算機的顯示器可以用來顯示 mainline build 的狀態(tài)。你很可能需要一個 build 令牌放在正在做 build 那人的桌子上(橡皮雞這種看上去傻傻的東西最好,原因同上)。有時人們會想在 build 成功時弄出一點噪音來,比如搖鈴的聲音。
持續(xù)集成服務(wù)器軟件的網(wǎng)頁可以承載更多信息。Cruise 不僅顯示誰在做 build,還能指出他們都改了什么。Cruise 還提供了一個歷史修改記錄,以便團隊成員能夠?qū)ψ罱椖坷锏那闆r有所了解。我知道 team leader喜歡用這個功能了解大家手頭的工作和追蹤系統(tǒng)的更改。
使用網(wǎng)站的另一大優(yōu)點是便于那些遠程工作的人了解項目的狀態(tài)。一般來說,我傾向于讓項目中發(fā)揮作用的成員都坐在一起工作,但通常也會有一些外圍人員想要了解項目的動態(tài)。如果組織想要把多個項目的 build情況聚合起來以提供自動更新的簡單狀態(tài)時,這也會很有用。
好的信息展示方式不僅僅依賴于電腦顯示器。我最喜歡的方式出現(xiàn)于一個中途轉(zhuǎn)入持續(xù)集成的項目。很長時間它都無法拿出一個穩(wěn)定的 build。我們在墻上貼了一整年的日歷,每一天都是一個小方塊。每一天如果 QA 團隊收到了一個能通過提交測試的穩(wěn)定 build,他們都會貼一張綠色的貼紙,否則就是紅色的貼紙。日積月累,從日歷上能看出 build 過程在穩(wěn)定地進步。直到綠色的小方塊已經(jīng)占據(jù)了大部分的空間時,日歷被撤掉了,因為它的使命已經(jīng)完成了。
自動化集成需要多個環(huán)境,一個運行提交測試,一個或多個運行次級測試。每天在這些環(huán)境之間頻繁拷貝可執(zhí)行文件可不輕松,自動化是一個更好的方案。為實現(xiàn)自動化,你必須有幾個幫你將應用輕松部署到各個環(huán)境中的腳本。有了腳本之后,自然而然的結(jié)果是你也要用類似的方式部署到生產(chǎn)環(huán)境中。你可能不需要每天都部署到生產(chǎn)環(huán)境(盡管我見過這么做的項目),但自動化能夠加快速度并減少錯誤。它的代價也很低,因為它基本上和你部署到測試環(huán)境是一回事。
如果你部署到生產(chǎn)環(huán)境,你需要多考慮一件事情:自動化回滾。壞事情隨時可能發(fā)生,如果情況不妙,最好的辦法是盡快回到上一個已知的正常狀態(tài)。能夠自動回滾也會減輕部署的壓力,從而鼓勵人們更頻繁地部署,使得新功能更快發(fā)布給用戶。(Ruby on Rails 社區(qū)開發(fā)了一個名為 Capistrano 的工具,是這類工具很好的代表。)
我還在服務(wù)器集群環(huán)境中見過滾動部署的方法,新軟件每次被部署到一個節(jié)點上,在幾小時時間內(nèi)逐步替換掉原有的軟件。
持續(xù)集成的益處在 web 應用開發(fā)中,我碰到的一個有趣的想法是把一個試驗性的 build 部署到用戶的一個子集。團隊可以觀察這個試驗 build 被使用的情況,以決定是否將它部署到全體用戶。你可以在做出最終決定之前試驗新的功能和新的 UI。自動化部署加上良好的持續(xù)集成的紀律是這項工作的基礎(chǔ)。
延遲集成的問題在于時間難以估計,你甚至無法得知你的進展。結(jié)果是你在項目最緊張的階段之一把自己置入了一個盲區(qū),此時即使沒有拖延(這很罕見)也輕松不了多少。我認為持續(xù)集成最顯著也最廣泛的益處是降低風險。說到這里,我的腦海中還是會浮現(xiàn)出第一段描述的早期軟件項目。他們已經(jīng)到了一個漫長項目的末期(至少他們期望如此),但還是不知道距離真正的結(jié)束有多遠。
Bug 讓人惡心,它摧毀人的自信,搞亂時間表,還破壞團隊形象。已部署軟件里的 bug 招致用戶的怒氣。未完成軟件里的 bug 讓你接下來的開發(fā)工作受阻。持續(xù)集成巧妙的解決了這個問題。長時間的集成不再存在,盲區(qū)被徹底消除了。在任何時間你都知道你自己的進展,什么能運轉(zhuǎn),什么不能運轉(zhuǎn),你系統(tǒng)里有什么明顯的 bug,這些都一目了然。
Bug 也會積累。你的 bug 越多,解決掉任何一個都會越困難。這部分原因是 bug 之間的互相作用,你看到的失敗實際上是多個問題疊加的結(jié)果,這使得檢查其中任何一個問題都更加困難。還有部分原因是心理層面的因素,當人們面對大量 bug 時,他們尋找和解決 bug 的動力就會減弱?!禤ragmatic Programmer》一書中稱之為“破窗綜合癥“。持續(xù)集成不能防止 bug 的產(chǎn)生,但它能明顯讓尋找和修改 bug 的工作變簡單。從這個方面看,它更像自測試代碼。如果你引入 bug 后能很快發(fā)現(xiàn),改正也會簡單得多。因為你只改了系統(tǒng)中很小的一部分,你無需看很多代碼就能找到問題所在。因為這一小部分你剛剛改過,你的記憶還很新鮮,也會讓找 bug 的工作簡單不少。你還可以用差異調(diào)試——比較當前版本和之前沒有 bug 的版本。
如果你用了持續(xù)集成,你就解決了頻繁部署的最大障礙之一。頻繁部署很有價值,因為它可以讓你的用戶盡快用到新功能,從而快速提供反饋,這樣他們在開發(fā)過程中可以有更多的互動。這可以幫助打破我心目中成功的軟件開發(fā)最大的障礙——客戶與開發(fā)團隊之間的障礙。使用持續(xù)集成的項目的通常結(jié)果是 bug 數(shù)目明顯更少,不管在產(chǎn)品里還是開發(fā)過程中都是如此。然而,我必須強調(diào),你受益的程度跟你測試的完善程度直接相關(guān)。其實建立測試系統(tǒng)并非想象中那么困難,但帶來的區(qū)別卻顯而易見。一般來說,團隊需要花一定時間才能把 bug 數(shù)量減少到理想的地步。做到這一點意味著不斷添加和改進測試代碼。
看到這里,你一定想要嘗試一下持續(xù)集成了。但是從哪里開始呢?我在上面描述了一整套的實踐,這些可以讓你體驗到所有的好處。但你也不必一開始就照單全收,你有自己的選擇余地,基本上取決于你的環(huán)境和團隊的特性。我們也從過去的實踐中吸取了一些經(jīng)驗和教訓,做好下面這些事會對于你的持續(xù)集成運作有重要的意義。
最早的幾步之一是實現(xiàn) build 自動化。把所有需要的東西都放進版本控制系統(tǒng)里,這樣你就可以用一條命令 build 整個系統(tǒng)。這對許多項目而言不是什么小任務(wù),這對于其他東西正常工作非常重要。剛開始你可能只是偶爾需要的時候做一個 build,或者只是做一個自動的 nightly build。當你還沒有開始持續(xù)集成時,自動 nightly build 也是一個不錯的開始。
其次是引入一些自動化測試到你的 build 中。試著指出主要出錯的地方,并要讓自動化測試暴露這些錯誤。建立又快又好的測試集合會比較困難,特別在已存在的項目中,建立測試需要時間。你必須找個地方開始動手,就像俗話說的,羅馬不是一天建成的。
還要試著加快提交 build 的速度。雖然需要幾個小時 build 的持續(xù)集成也比什么都沒有好,但能做到傳說中的10分鐘會更好。這通常要對你的代碼動一些大手術(shù),以剝離對運行緩慢那部分的依賴。
如果你剛剛開始一個新項目,從一開始就用持續(xù)集成。對build時間保持關(guān)注,當慢于10分鐘時就立即采取行動。通過快速行動,你可以在代碼變得太大之前做一些必要的架構(gòu)調(diào)整。
比所有事情都重要的是尋找?guī)椭?。找一個以前做過持續(xù)集成的人來幫你。像所有新技巧一樣,當你不知道最終結(jié)果怎樣的時候會非常難以實施。請一個導師(mentor)可能會花些錢,但如果你不做,你會付出時間和生產(chǎn)效率損失的代價。(免責聲明/廣告:是的,我們 ThoughtWorks 在這個領(lǐng)域提供咨詢服務(wù)。不管怎樣,我們曾經(jīng)犯過你可能會犯的大多數(shù)錯誤。)
在我和 Matt 寫完最初那篇論文后的幾年,持續(xù)集成已經(jīng)成為了軟件開發(fā)的一個主流方法。ThoughtWorks 的項目很少有不用到它的。我們也能看到世界各地的人們在用持續(xù)集成。與極限編程中一些充滿爭議的實踐不同,我很少聽到關(guān)于持續(xù)集成的負面消息。
如果你還沒用持續(xù)集成,我強烈建議你試一下。如果你已經(jīng)在做了,可能這篇文章中的某些方法可以幫你得到更好的效果。過去幾年中,我們已經(jīng)了解了很多關(guān)于持續(xù)集成的知識,我希望還有更多的知識可以讓我們學習和提高。
本文篇幅所限,只能覆蓋部分內(nèi)容。想了解持續(xù)集成的更多細節(jié),我建議看一下 Paul Duvall 的書(這本書得到了Jolt大獎)。目前為止還沒有多少關(guān)于分階段build的文章,但有一篇 Dave Farley 發(fā)表在《ThoughtWorks 文選》中的文章還不錯(你也可以在這里找到)。