閱讀56 返回首頁    go 人物


《Google軟件測試之道》—第2章2.1節SET的工作

本節書摘來自異步社區《Google軟件測試之道》一書中的第2章2.1節SET的工作,作者【美】James Whittaker , Jason Arbon , Jeff Carollo,更多章節內容可以訪問雲棲社區“異步社區”公眾號查看。

第2章 軟件測試開發工程師
Google軟件測試之道
C:\Documents and Settings\Administrator\桌麵\頁麵提取自- 9780321803023_book.jpg

在理想情況下,一個完美的開發過程是怎樣進行的呢?測試先行,在一行代碼都沒有真正編寫之前,一個開發人員就會去思考如何測試他即將編寫的代碼。他會設計一些邊界場景的測試用例,數據取值範圍從極大到極小、導致循環語句超出限製範圍的情況,另外還會考慮很多其他的極端情況。這些測試代碼會作為產品代碼的一部分,以自檢代碼或單元測試代碼的形式與功能代碼存儲在一起。對於此種類型的測試,最合適且最有資格去做的人,其實就是編寫功能代碼的人。

另外一些測試需要的知識在本產品代碼之外,通常都依賴於外部基礎設施服務。例如,一個測試用例需要從遠程數據源(一個數據庫或者雲端)讀取數據,這就需要存在一個真實數據庫或模擬的數據庫。在過去幾年中,工業界使用了各種特定術語來描述這些輔助設施,包括測試框架、測試通用設施、模擬設施和虛擬設施(譯注:test harnesses, test infrastructure, mock and fake)。在假想的完美開發過程中,在你做功能測試時,如果需要,這些工具都應該及時出現在你眼前,任由你使用(記住,這是在一個真正理想的軟件世界裏)。

在理想開發過程中首次需要測試人員的時刻即將來臨。對於人的思維方式而言,在編寫功能代碼的時候與編寫測試代碼的時候是迥然不同的,這也就需要去區分功能開發人員和測試開發人員(譯注:原文是feature developer and test developer)。對於功能代碼而言,思維模式是創建,重點在考慮用戶、使用場景和數據流程上;而對於測試代碼來說,主要思路是去破壞,怎樣寫測試代碼用以擾亂分離用戶及其數據。由於我們假設的前提是在一個童話般的理想開發過程裏,所以我們或許可以分別雇傭不同的開發工程師:一個寫功能代碼,而另一個思考如何破壞這些功能(譯注:兩種開發工程師,分別是功能開發人員和測試開發人員)。

注意
編寫功能代碼和編寫測試代碼在思維方式上有著很大的不同。
在這樣烏托邦式(譯注:烏托邦是一個理想的群體和社會的構想,名字由托馬斯·摩爾的《烏托邦》一書中所寫的完全理性的共和國“烏托邦”而來,意指理想完美的境界)的理想開發過程中,眾多的功能開發人員(譯注:feature developer)和測試開發人員(譯注:test developer)需要通力合作,共同為打造同一款產品而努力。在我們假想的完美理想情況下,產品的每一個功能都對應一個開發人員,整個產品則配備一定數量的測試開發人員。測試開發人員通過使用測試工具與框架幫助功能開發人員解決特定的單元測試問題,而這些問題如果隻是由功能開發人員獨自完成,則會消耗掉他們許多的精力。

功能開發人員在編寫功能代碼的時候,測試開發人員編寫測試代碼,但我們還需要第三種角色,一個關心真正用戶的角色。顯然在我們理想化的烏托邦測試世界裏,這個工作應該由第三種工程師來完成,既不是功能開發人員,也不是測試開發人員。我們把這個新角色稱為用戶開發人員(譯注:user developer)。他們需要解決的主要問題是麵向用戶的任務,包括用例(use case)、用戶故事、用戶場景、探索式測試等。用戶開發人員關心這些功能模塊如何集成在一起成為一個完整的整體,他們主要考慮係統級別的問題,通常情況下都會從用戶角度出發,驗證獨立模塊集成在一起之後是否對最終用戶產生價值。

這就是我們眼中軟件開發過程的烏托邦理想模式,三種開發角色在可用性和可靠性方麵分工合作,達到完美。每個角色專門處理重要的事情,相互之間又可以平等地合作。

誰不想為這樣的軟件開發公司工作呢?大家全都要報名應聘!

但不幸的是,這樣的公司目前還不存在,Google也隻是比較接近而已。Google與其他公司一樣,都在盡力去嚐試成為這樣的公司。或許是因為Google起步較晚,我們有機會從前人那裏吸取了很多經驗教訓。當前軟件正經曆一個巨大的轉變,從發布周期需要以年為單位的客戶端模式向每周、每天,甚至每小時都會發布的雲端模式轉變(注:一個有趣的事情需要說明一下,即使是客戶端軟件,Google也喜歡常去更新,客戶端使用一個“自動更新”的功能,幾乎所有的客戶端應用都有這個功能),而Google也從這次轉換浪潮之中受益良多。在這兩種原因的促進下,Google的軟件開發流程與烏托邦模式也有了幾分相似。

Google的SWE就是功能開發人員,負責客戶使用的功能模塊開發。他們編寫功能代碼及這些功能的單元測試代碼。

Google的SET就是測試開發人員,部分職責是在單元測試方麵給予開發人員支持,另外一部分職責是為開發人員提供測試框架,以方便他們編寫中小型測試,用以進行更多質量相關的測試工作。

Google的TE就是用戶開發人員,負責從用戶的角度來思考質量方麵各種問題。從開發的角度來看,他們編寫用戶使用場景方麵的自動化用例代碼;從產品的角度看,他們評估整體測試覆蓋度,並驗證其他工程師角色在測試方麵合作的有效性。這不是烏托邦,這就是Google實踐之路上最好的嚐試,前進的道路上充滿了不可預料且無路可退。

注意
Google的SWE是功能開發人員;Google的SET是測試開發人員;Google的TE是用戶開發人員。
在這本書裏,我們將會著重介紹SET和TE這兩個角色的工作內容,也會包含少量SWE的工作內容,作為上述兩種角色的補充。雖然SWE也重度參與測試工作,但一般情況下都是在頭銜中包含“測試”的工程師的指導之下完成的。

2.1 SET的工作
在任何軟件公司創立的初期階段,通常都沒有專職的測試人員(譯注:本節標題“SET的工作”,因為原文為The Life of an SET。“The Life of ”是Google內部係列課程(搜索和廣告是如何工作的)中使用的特定術語。針對Nooglers(新Google員工)的課程裏,Life of a Query揭秘搜索query是如何實現的,Life of a Dollar揭秘廣告係統的工作原理)。當然那時候也沒有產品經理、計劃人員、發布工程師、係統管理員等其他角色。每位員工都獨自完成所有工作。我們也經常想象Larry和Sergey(譯注:Google的早期創始人之一)在早期是如何思考用戶使用場景和設計單元測試的樣子。隨著Google的不斷成長壯大,出現了第一個融合開發角色和質量意識於一身的角色,即SET(注:Patrick Copeland在本書的序中已經介紹了SET的出現背景)。

2.1.1 開發和測試流程
在詳細講解SET工作流程之前,我們先來了解一下SET的工作背景,這對理解整個開發過程將十分有益。在新產品的開發過程中,SET和SWE是緊密合作的夥伴,他們達成一致,甚至一些實際工作也會有所重疊。Google其實就是這樣設計的,Google認為測試工作是由整個工程團隊負責,而不僅僅單獨由那些頭銜上帶著“測試”的工程師來負責。

工程師團隊的交付物就是即將發布的代碼。代碼的組織形式、開發過程、維護是日常工作重點。Google多數代碼存放在同一個代碼庫中,並使用統一的一套工具。這些工具和代碼支撐著Google的構建和發布流程。Google所有的工程師無論是什麼角色,對如何使用這些工具環境都非常地熟練,團隊成員可以毫不費力地完成新代碼的入庫、提交、執行測試、創建版本等任務(前提是角色有這樣的需求)。

注意
工程師團隊的交付物就是即將要發布的代碼。代碼的組織形式、開發過程、維護是日常的工作重點。
這種單一的代碼庫模式,使得工程師可以很從容地在不同項目之間轉換而幾乎不需要什麼學習成本。這為工程師提供了很大便利,這種單一的代碼庫模式讓工程師從他們進入項目開始的第一天起,其“百分之二十的貢獻”(譯注:“百分之二十時間”是指Googler稱為的“業餘項目”。這並不是一個炒作的概念,而是官方真正存在的,允許所有Googler每周投入一天時間在他的日常工作之外的項目上。每周四天工作用來賺取薪水,剩下一天用以試驗和創新。這並不是完全強製的,之前有些Googler認為這個想法隻是一個傳說。根據我們的真實經曆,這個概念是真正存在的,我們三個都參與過“百分之二十時間”項目。實際上,本書提及的許多工具都是“百分之二十”項目的結晶。在現實中,許多Goolers選擇把“百分之二十時間”投入到新產品之中,特別是一些聽起來很酷的產品,很享受這種工作模式)極具效率。這也意味著對於有需求的工程師,所有的源代碼對他們都是開放的。Web 應用的開發人員無須申請任何權限,就能查看所有可以簡化他們工作的瀏覽器端代碼。他們從有經驗的工程師那裏學習到在類似場景下如何編寫代碼,他們可以重用一些通用模塊或詳細的數據結構,甚至是重用一些程序控製結構。Google在代碼庫搜索方麵也提供了非常便利的功能。

公開的代碼庫、和諧的工程工具、公司範圍內的資源共享,成就了豐富的Google內部共享代碼庫與公共服務。這些共享的代碼運行依賴於Google的基礎設施產品,它們在加速項目完成與減少項目失敗上發揮了很大作用。

注意
公開的代碼庫、和諧的工程工具、公司範圍內的資源共享,成就了豐富的Google內部共享代碼庫與公共服務。
工程師們對這些共享的基礎代碼做了特殊處理,形成了一套不成文但卻非常重要的實踐規則,工程師在維護修改這些代碼的時候都要遵守這些規則。

所有的工程師必須複用已經存在的公共庫,除非在項目特定需求方麵有很好的理由。
對於公共的共享代碼,首先要考慮的是能否可以容易地被找到,並具有良好的可讀性。代碼必須存儲在代碼庫的共享區域,以便查找。由於共享代碼會被不同的工程師使用,這些代碼應該容易理解。所有的代碼都要考慮到未來會被其他人閱讀或修改。
公共代碼必須盡可能地被複用且相對獨立。如果一個工程師提供的服務被許多團隊使用,這將為他帶來很高的信譽。與功能的複雜性或設計的巧妙性相比,可複用性帶來的價值更大。
所有依賴必須明確指出,不可被忽視。如果一個項目依賴一些公用共享代碼,在項目工程師不知情的前提下,這些共享代碼是不允許被修改的。
如果一個工程師對共享代碼庫在某些地方有更好的解決方案,他需要去重構已有的代碼,並協助依賴在這個公用代碼庫之上的應用項目遷移到新的代碼庫上。這種樂善好施的社區工作是值得鼓勵的(譯注:這是Google經常提及的“同僚獎金(peer bonus)”。任何工程師如果受到其他工程師正麵的影響,就可以送出“同僚獎金”作為感謝。除此之外,經理還有權使用其他獎勵手段。這樣做的目的就是讓這種正向團隊合作形成一種良性循環,並持續下去。當然,另外還有同事之間私下裏的感謝)。
Google非常重視代碼審核,特別是公共通用模塊的代碼必須經過審核。開發人員必須通過相關語言的可讀性審核。在開發人員擁有按照代碼風格編寫出幹淨代碼的記錄之後,委員會會授予這名開發人員一個“良好可讀性”的證書。Google的四大主要開發語言:C++、Java、Python和JavaScript都有可讀性方麵的代碼風格指南。
在共享代碼庫裏的代碼,對測試有更高的要求(在後麵部分會做討論)。
最小化對平台的依賴。所有工程師都有一台桌麵工作機器,且操作係統都盡可能地與Google生產環境的操作係統保持一致。為了減少對平台的依賴,Google對Linux發行版本的管理也十分謹慎,這樣開發人員在自己工作機器上測試的結果,與生產係統裏的測試結果會保持一致。從桌麵到數據中心,CPU和OS的變化盡可能小(注:唯一不在Google通用測試平台裏的本地測試實驗室,是Android和Chrome OS。這些類目不同的硬件必須在手邊進行測試)如果一個bug在測試機器上出現,那麼在開發機器上和生產環境的機器上也都應該能夠複現。

所有對平台有依賴的代碼,都會強製要求使用公共的底層庫。維護Linux發行版本的團隊同時也在維護這個底層平台相關的公共庫。還有一點,對於Google使用的每個編程語言,都要求使用統一的編譯器,這個編譯器被很好地維護著,針對不同的Linux發行版本都會有持續的測試。這樣做本身其實並沒有什麼神奇之處,但限製運行環境可以節省大量下遊的測試工作,也可以避免許多與環境相關且難以調試的問題,能把開發人員的重心轉移到新功能開發上。保持簡單,也就相對會安全。

注意
Google在平台方麵有特定的目標,就是保持簡單且統一。開發工作機和生產環境的機器都保持統一的Linux發行版本;一套集中控製的通用核心庫;一套統一的通用代碼、構建和測試基礎設施;每個核心語言隻有一個編譯器;與語言無關的通用打包規範;文化上對這些共享資源的維護表示尊重且有 激勵。
使用統一的運行平台和相同的代碼庫,持續不斷地在構建係統中打包(譯注:打包是一個過程,包括將源代碼編譯成二進製文件,然後再把二進製文件統一封裝在一個linux rpm包裏麵),這可以簡化共享代碼的維護工作。構建係統要求使用統一的打包規範,這個打包規範與項目特定的編程語言無關,與團隊是否使用C++、Python或Java也都無關。大家使用同樣的“構建文件”來打包生成二進製文件。

一個版本在構建的時候需要指定構建目標,這個構建目標(可以是公共庫、二進製文件或測試套件)由許多源文件編譯鏈接產生。下麵是整體流程。

(1)針對某個服務,在一個或多個源代碼文件中編寫一類或一係列功能函數,並保證所有代碼可以編譯通過。

(2)把這個新服務的構建目標設定為公共庫。

(3)通過調用這個庫的方式編寫一套單元測試用例,把外部重要依賴通過mock模擬實現。對於需要關注的代碼路徑,使用最常見的輸入參數來驗證。

(4)為單元測試創建一個測試構建目標。

(5)構建並運行測試目標,做適當的修改調整,直到所有的測試都運行成功。

(6)按要求運行靜態代碼分析工具,確保遵守統一的代碼風格,且通過一係列常見問題的靜態掃描檢測。

(7)提交代碼申請代碼審核(後麵對代碼審核會做更多詳細說明),根據反饋再做適當的修改,然後運行所有的單元測試並保證順利通過。

產出將是兩個配套的構建目標:庫構建目標和測試構建目標。庫構建目標是需要新發布的公共庫、測試構建目標用以驗證新發布的公共庫是否滿足需求。注意:在Google許多開發人員使用“測試驅動開發”的模式,這意味著步驟(3)會在步驟(1)和步驟(2)之前進行。

對於規模更大的服務,通過鏈接編譯持續新增的代碼,構建目標也會逐漸變大,直到整個服務全部構建完成。在這個時候,會產生二進製構建目標,其由包含主入口main函數文件和服務庫鏈接在一起構成。現在,你完成了一個Google產品,它由三部分組成:一個經過良好測試的獨立庫、一個在可讀性與可複用性方麵都不錯的公共服務庫(這個服務庫中還包含另外一套支持庫,可以用來創建其他的服務)、一套覆蓋所有重要構建目標的單元測試套件。

一個典型的Google產品由許多服務組成,所有產品團隊都希望一個SWE負責對應一個服務。這意味著每個服務都可以並行地構建、打包和測試,一旦所有的服務都完成了,他們會在一個最終的構建目標裏一起集成。為了保證單獨的服務可以並行地開發,服務之間的接口需要在項目的早期就確定下來。這樣,開發者會依賴在協商好的接口上,而不是依賴在需要開發的特定庫上。為了不耽擱服務級別之間的早期測試,這些接口一般都不會真正實現,而隻是做一個虛假的實現。

SET會參與到許多測試目標的構建之中,並指出哪些地方需要小型測試。在多個構建目標集成在一起,形成規模更大應用程序的構建目標時,SET需要加速他們的工作,開始做一些更大規模的集成測試。在一個單獨的庫構建目標中,需要運行幾乎所有的小型測試(由SWE編寫,所有支持這個項目的SET都會給予幫助)。當構建目標日益增大時,SET也會參與到中大型測試的編寫之中去。

在構建目標的增長到一定規模時,針對功能集成的小型測試會成為回歸測試的一部分。如果一個測試用例,本應該運行通過,但如果運行失敗,也會報一個測試用例的bug。這個針對測試用例的bug和針對功能的bug沒有任何區別。測試就是功能的一部分,問題較多的測試就是功能性bug,一定要得到修複。這樣才可以保證新增的功能不會把已有功能損壞掉,任何代碼的修改都不會導致測試本身的失敗。

在所有的這些活動中,SET始終是核心參與者。他們在開發人員不知道哪些地方需要單元測試的時候可以明確指出。他們同時編寫許多mock和fake工具。他們甚至編寫中大型集成測試。好了,現在是展開討論SET工作的時候了。

2.1.2 SET究竟是誰
SET首先是工程師角色,他使得測試存活於先前討論的所有Google開發過程之中。SET(software engineer in test)是軟件測試開發工程師。最重要的一點,SET是軟件工程師,正如我們招聘宣傳海報和內部晉升體係中所說的那樣,是一個100%的編碼角色。這種測試方式的有趣之處在於它使測試人員能盡早介入到開發流程中去,但不是通過“質量模型”和“測試計劃”的方式,而是通過參與設計和代碼開發的方式。這會使得功能的開發工程師和測試的開發工程師處於相同的地位,SET積極參與各種測試,使測試富有效率,包括手動測試和探索式測試,而這些測試後期會由其他工程師負責。

注意
測試是應用產品的另外一種功能,而SET就是這個功能的負責人。
SET與功能開發人員坐在一起(實際上,讓他們物理位置坐在一起是也是我們的設計目標)。這樣講可能更公平一些,測試也是應用產品的一種功能特性,而SET是這個產品功能特性的負責人。SET參與SWE的代碼評審,反之亦然。

在麵試SET的時候,在代碼要求標準上與SWE的招聘要求是一樣的,而且增加了一個額外考核——SET需要了解如何去測試他們編寫的代碼。換句話說,SWE和SET都需要回答代碼問題,而且SET還要求去解答測試問題。

正如你想象的那樣,找到滿足如此條件的人是非常困難的,在Google,SET的數量也相對比較少,這並不是因為Google在生產率方麵有什麼神奇的開發測試比要求,而是因為招聘到滿足SET技能要求的人實在太難了。SWE和SET這兩個角色比較相似,在招聘方麵這兩個群體的要求也類似。假想這樣的場景,公司裏的開發人員可以做測試,而測試人員可以寫代碼。Google其實還沒有完全做到這一點,或許永遠也做不到。這兩大群體之間相互交流學習,SWE向SET學習,SET也在學習SWE,正是我們這些最優秀的工程師一起構成了我們最有效率的工程產品團隊。

2.1.3 項目的早期階段
Google沒有規定SET何時進入項目,同樣也沒有規定怎樣的項目才算是“真正”的項目。通常情況下,在Google的產品項目初期階段,工程師隻會投入20%的時間。Gmail和Chrome OS也是從一個想法演變而來,初期也並沒有任何Google官方資源的投入,這些資源來源於團隊開發測試成員的業餘時間。事實上也正如我們的朋友Alberto Savoia(本書的序言的作者之一,詳細介紹參見序部分)所說的那樣,“隻有在軟件產品變的重要的時候質量才顯得重要”。

許多創新的產品都是來源於團隊20%的業餘時間。這些時間投入的產品有些慢慢地消失了,而另外一些規模會越做越大,有的甚至會成為Google的官方產品。在這些產品的初期,沒有一個會得到測試資源。在未來可能失敗的項目中投入測試資源來構建測試方麵基礎設施,這是一種資源浪費。如果項目被取消了,那麼這些創建好的測試也會毫無價值。

一個產品如果在概念上還沒有完全確定成型時就去關心質量,這就是優先級混亂的表現。許多來源於Google百分之二十努力的產品原型,在其以後的dogfood或beta版本發布時,還要經曆重新設計,原始代碼保留的概率幾乎為零。很明顯,在試驗初期階段強調測試是一件非常愚蠢的事情。

當然,物極必反,風險總是相對的。如果一個產品太長時間沒有測試的介入,早期在可測試性上的槽糕設計在後期也很難去做改進,這樣會導致自動化難以實施且測試工具極不穩定。在這種情況下,不得不以質量的名義來做重構。這樣的質量“債”會拖慢產品的發布,甚至長達數年之久。

在項目早期,Google一般不會讓測試介入進來。實際上,即使SET在早期參與進來,也不是從事測試工作,而是去做開發。絕非有意忽視測試,當然也不是說早期產品的質量就不重要。這是受Google非正式創新驅動產品的流程所約束。Google很少在項目創建初期就投入一大幫人來做計劃(包括質量與測試計劃),然後再讓一大群開發參與進來。Google項目的誕生從來沒有如此正式過。

Chrome OS是一個可以說明問題的典型例子。本書的三個作者都在這個產品上工作過一年以上。但是,在我們正式加入之前,隻有幾個開發人員做了原型,且多數實現都是腳本與偽件(fake),這樣他們可以拿著瀏覽器應用模型做演示,並通過正式的立項批準。在這些早期原型階段,主要精力都集中在如何試驗並證明這些想法的可行性上。考慮到項目還沒有正式批準,且所有的演示腳本最終都會被C++代碼重寫替換,如果在早期投入大量測試和可測試性方麵努力,其實沒有太大的實用價值。為了演示而使用腳本搭建的產品,一旦得到正式批準立項,其開發總監就會找到工程生產力團隊,尋求測試資源。

Google內部其實也並存著不同的文化。沒有項目會認為如果得不到測試資源,他們的產品就將不複存在。開發團隊在尋求測試幫助的時候,有義務讓測試人員相信他們的產品是令人興奮且並充滿希望的。在Chrome OS的開發總監給我們介紹他們項目、進度和發布計劃時,我們也要求提供當前已有的測試狀態、期望的單元測試覆蓋率水平、以及明確在發布過程中各自承擔的責任。在項目還是概念階段的時候,測試人員不會參與進來,而項目一旦真正立項,我們就要在這些測試是如何執行的方麵發揮我們的影響力。

注意
沒有項目會認為如果得不到測試資源,他們的產品就將不複存在。開發團隊在尋求測試幫助的時候,有義務讓測試人員相信他們的產品是令人興奮且並充滿希望的。
2.1.4 團隊結構
SWE會深入他們自己編寫的那部分代碼之中,通常這部分代碼隻是某個單一功能的模塊甚至更小範圍的代碼。SWE一般僅在自己的模塊領域裏提供最優方案,但如果從整個產品的角度來看,視野會顯得略微狹窄。一個好的SET正好可以彌補這一點,不僅要具有更寬廣的整體產品視野,而且在產品的整個生命周期裏對產品及功能特性做充分理解,許多SWE來往穿梭於不同產品,但產品的生命存活期比SWE待在產品裏的時間要長久得多。

像Gmail或Chrome這樣的產品注定要經曆許多版本,並消耗數以百計的開發人員為之工作。如果一個SWE在某個產品的第三個版本研發時加入,這時這個產品已經有良好的文檔、不錯的可測試性、運行著穩定的自動化測試、清晰的代碼提交流程,這些現象都在說明這個產品早期已有出色的SET在為之工作。

在整個項目生命周期裏,功能的實現、版本的發布、補丁的創建、為改進而做的重構在不斷地發生,你很難說清楚什麼時候項目結束或一個項目是否真的已經結束。但所有軟件項目都有明確的開始時間。在早期階段,我們常去改變我們的目標。我們做計劃,並嚐試把東西做出來。我們嚐試去文檔化我們將要去做的事情。我們嚐試去保證我們早期做的決定長期看來也是正確的。

我們在編碼之前做計劃、試驗、文檔,這部分工作量取決於我們對未來產品的信心。我們不想在項目初期做少量的計劃,而到項目後期卻發現這個計劃是值得花費更多精力去做的。同樣,我們也不希望在早期計劃上投入數周時間,而之後卻發現這個世界已經改變了,甚至與之前我們想象的世界完全不同了。某種程度上來說,我們早期在文檔結構和過程中的處理方式也是明智的。總而言之,做多少和怎樣做比較合適,由創建項目的工程師來做最終決定。

Google產品團隊最初是由一個技術負責人(tech lead)和一個或更多的項目發起人組成。在Google,技術負責人這個非正式的崗位一般由工程師擔任,負責設定技術方向、開展合作、充當與其他團隊溝通的項目接口人。他知道關於項目的任何問題,或者能夠指出誰知道這些問題的細節。技術負責人通常是一名SWE,或者由一名具備SWE能力的工程師來擔任。

項目的技術負責人和發起人要做的第一件事就是設計文檔(後文會做介紹)。隨著文檔的不斷完善,就需要不同專業類型的工程師角色投入到項目中去。許多技術負責人期望SET在早期就能參與項目,即便那時SET資源還相對稀缺。

2.1.5 設計文檔
所有Google項目都有設計文檔。這是一個動態的文檔,隨著項目的演化也在不斷地保持更新。最早期的項目設計文檔,主要包括項目的目標、背景、團隊成員、係統設計。在初期階段,團隊成員一起協同完成設計文檔的不同部分。對於一些規模足夠大的項目來說,需要針對主要子係統也創建相應的設計文檔,並在項目設計文檔中增加子係統設計文檔的鏈接。在初期版本完成後,裏麵會囊括所有將來需要完成的工作清單,這也可以作為項目前進的路標。從這一點上講,設計文檔必須要經過相關技術負責人的審核。在項目設計文檔得到足夠的評審與反饋之後,初期版本的設計文檔就接近尾聲了,接下來項目就正式進入實施階段。

作為SET,比較幸運的是在初期階段就加入了項目,會有一些重要且有影響力的工作急需完成。如果能夠合理地謀劃策略,我們在加速項目進度的同時,也可以做到簡化項目相關人員的工作。實際上,作為工程師,SET在團隊中有一個巨大的優勢,就是擁有產品方麵最廣闊的視野。一個好的SET會把非常專業的廣闊視野轉化成影響力,在開發人員所編寫的代碼上產生深遠的影響力。通常來說,代碼複用和模塊交互方麵的設計會由SET來做,而不是SWE。後麵會著重介紹SET在項目的初期階段是如何發揮作用的。

注意
在設計階段,SET在推進項目的同時也可以簡化相關項目成員的工作。
如果有另外一雙眼睛來幫助審核你的工作,這是無疑會很有幫助且令人期待。SWE就渴望得到來自SET的這種幫助與反饋。在SWE完成設計文檔的各個部分之後,需要發送給更大範圍人去做正式審核,在這之前他們希望得到SET的幫助。一個優秀的SET對這樣的文檔審核也會比較期待,樂意去投入他的時間,在SET審閱過程中,會針對質量和可靠性方麵增加一些必要的內容。下麵是我們為什麼這麼做的幾個原因。

SET需要熟悉了解所負責的係統設計(閱讀所有的設計文檔是一個途徑),SET和SWE都期望如此。
SET早期提出的建議會反饋在文檔和代碼裏,這樣也增加了SET的整體影響力。
作為第一個審閱所有設計文檔的人(也因此了解所有迭代過程),SET對項目的整體了解程度超過了技術負責人。
對於SET來說,這也是一個非常好的機會,可以在項目初期就與相應開發工程師一起建立良好的工作關係。
審閱設計文檔的時候應該有一定的目的性,而不是像讀報紙那樣隨便看兩眼就算了。優秀的SET在審閱過程中始終保持強烈的目的性。下麵是一些我們推薦的一些要點。

完整性:找出文檔中殘缺不全或一些需要特殊背景知識的地方。通常情況下團隊裏沒人會了解這些知識,特別是對新人而言。鼓勵文檔作者在這方麵添加更多細節,或增加一些外部文檔鏈接,用以補充這部分背景知識。
正確性:看一下是否有語法、拚寫、標點符號等方麵的錯誤,這一般是馬虎大意造成的,並不意味著他們以後編寫的代碼也是這樣。但也不能為這種錯誤而破壞規矩。
一致性:確保配圖和文字描述一致。確保文檔中沒有出現與其他文檔中截然相反的觀點和主張。
設計:文檔中的一些設計要經過深思熟慮。考慮到可用的資源,目標是否可以順利達成?要使用何種基礎的技術框架(讀一讀框架文檔並了解他們的不足)?期望的設計在框架方麵使用方法上是否正確?設計是否太過複雜?有可能簡化嗎?還是太簡單了?這個設計還需要增加什麼內容?
接口與協議:文檔中是否對所使用的協議有清晰的定義?是否完整地描述了產品對外的接口與協議?這些接口協議的實現是否與他們期望的那樣一致?對於其他的Google產品是否滿足統一的標準?是否鼓勵開發人員自定義Protocol buffer數據格式(後麵會討論Protocol buffer)?
測試:係統或文檔中描述的整套係統的可測試性怎樣?是否需要新增測試鉤子(譯注:testing hook,這裏指為了測試而增加一些接口,用以顯示係統內部狀態信息)?如果需要,確保他們也被添加到文檔之中。係統的設計是否考慮到易測試性,而為之也做了一些調整?是否可以使用已有的測試框架?預估一下在測試方麵我們都需要做哪些工作,並把這部分內容也增加到設計文檔中去。

注意
審閱設計文檔的時候要,具備一定的目的性,需要完成特定的目標,而不是像讀報紙那樣隨意看兩眼。
在SET與相應的SWE一起溝通文檔的審閱結果時,關於測試的工作量以及各個角色之間如何共同參與測試,會有一個比較正式的討論。這是一個絕佳的時機,可以了解到開發在單元測試方麵的目標,以及如果想打造一款經過良好測試的產品,團隊成員需要遵守哪些最佳實踐。當這種討論以互幫互助的形式開始出現時,我們的工作就開始逐步進入正軌了。

2.1.6 接口與協議
在Google,由於接口協議與編寫代碼相關,所以對於開發人員來說,文檔化這部分是比較輕鬆的事情。Google protocol buffer語言(注:Google protocol buffers 是開源的,參見https://code.google.com/apis/protocolbuffers)與編碼語言和平台無關,對結構化數據而言具有可擴展性,就像XML一樣,但更小、更快、更簡單。開發人員使用protocol buffer的描述語言來定義數據結構,然後使用自動生成的源代碼,從各種數據流中來讀或寫這些結構化的數據,使用任何編程語言(Java, C++或python)皆可。對於新項目而言,protocol buffer源碼通常是第一份源代碼。在係統實現之後,如果設計文檔中仍然使用protocol buffers來描述係統是如何工作的,這比較罕見。

SET會對protocol buffer代碼做比較係統全麵的審查,因為protocol buffer定義的接口與協議的代碼實現是要由SET來完成的。沒錯,SET是第一個實現所有接口和協議的人。在係統真正搭建起來之前,集成測試的運行依賴這些接口實現。為了能夠盡早地開始做集成測試,SET針對各個模塊的依賴提供了mock或fake的實現。雖然功能模塊代碼還沒有實現,集成測試的代碼就已經可以開始編寫了。在這個時候,如果集成測試代碼可以運行起來,那將會更有價值。另外,在任何階段,集成測試總是依賴mock和fake。因為有了它們,一些依賴服務的期望錯誤場景和條件異常,會比較容易產生。

注意
為了能夠盡早可以運行集成測試,針對依賴服務,SET提供了mock與fake。

2.1.7 自動化計劃
SET時間有限且需要做的事情太多,盡早地提供一個可實施的自動化測試計劃是一個很好的解決方法。試圖在一個測試套件中自動化所有端到端的測試用例,這是一個常見的錯誤。沒有SWE會被這樣一個無所不包的設計所吸引並感興趣,SET也就得不到SWE的什麼幫助。如果SET希望能從SWE那裏得到幫忙,他的自動化計劃就必須合情合理且有影響力。自動化上投入的越多,維護的成本也就越大。在係統升級變化時,自動化也會更加不穩定。規模更小且目的性更強的自動化計劃,並存在可以提供幫助的測試框架,這些會吸引SWE一起參與測試。

在端到端的自動化測試上過度投入,常常會把你與產品的特定功能設計綁定在一起,這部分測試在整個產品穩定之前都不會特別有用。在產品完成之後,這個時候如果去修改設計就已經太晚了。所以,這個時刻從測試中得到的任何反饋也將變得毫無意義。SET的時間,本應投入在提高質量方麵,卻白白地花費在維護這些不穩定的端到端測試套件上。

注意
在端到端自動化測試上過度投入,常常會把你與產品的特定功能設計綁定在一起。
在Google,SET遵循了下麵的方法。

我們首先把容易出錯的接口做隔離,並針對它們創建mock和fake(在之前的章節中做過介紹),這樣我們可以控製這些接口之間的交互,確保良好的測試覆蓋率。

接下來構建一個輕量級的自動化框架,控製mock係統的創建和執行。這樣的話,寫代碼的SWE可以使用這些mock接口來做一個私有構建。在他們把修改的代碼提交到代碼服務器之前運行相應的自動化測試,可以確保隻有經過良好測試的代碼才能被提交到代碼庫中。這是自動化測試擅長的地方,保證生態係統遠離糟糕代碼,並確保代碼庫永遠處於一個時刻幹淨的狀態。

SET除了在這個計劃中涵蓋自動化(mock、fake和框架)之外,還要包括如何公開產品質量方麵的信息給所有關心的人。在Google,SET使用報表和儀表盤(譯注:dashboard)來展示收集到的測試結果以及測試進度。通過將整個過程簡化和信息公開透明化,獲取高質量代碼的概率會大大增加。

2.1.8 可測試性
在產品開發過程中,SWE和SET緊密地工作在一起。SWE編寫產品代碼並測試這些代碼。SET編寫測試框架,為SWE編寫測試代碼方麵提供幫助。另外,SET也做一些維護工作。質量責任由SWE和SET共同承擔。

SET的第一要務就是可測試性。SET在扮演一個質量顧問的角色,提供程序結構和代碼風格方麵的建議給開發人員,這樣開發人員可以更好地做單元測試。同時提供測試框架方麵的建議,使得開發人員能夠在這些框架的基礎上自己寫測試。後麵我們再討論框架,在這裏讓我們首先說一下Google的代碼流程。

作為開發人員,一個基本的要求就是有能力做代碼審查。代碼審查需要工具和文化方麵的支持,這個文化習俗來源於開源社區中“提交者”的概念,隻有被證明是值得信賴的開發者之後,才具有往代碼庫中提交代碼的資格。

注意
為了使SET也成為源碼的擁有者之一,Google把代碼審查作為開發流程的中心。相比較編寫代碼而言,代碼審查更值得炫耀。
在Google,每個人都是代碼提交者。但是,我們使用了另外一個詞“可讀性”來區分有已被證明有資格的提交者和新開發人員。下麵介紹整個流程如何工作的。

代碼以一個被稱為“變更列表”(譯注:change list,下文簡寫CL)的單元被編寫和封裝起來。CL在編碼結束之後會提交審查,其中使用一個Google內部工具Mondrian(以一個荷蘭抽象派畫家為名)。Mondrian會把需要審查的代碼發送給具有審閱資格的SWE或SET,並最終通過代碼審查(譯注:在Google App Engine上運行著一個開源版本的Mondrian,參見https://code.google.com/p/rietveld/)。

CL可以是一段新代碼,也可以是對已有代碼的修改,或是缺陷修複等。CL代碼的大小從幾行到幾百行不等,一般審查者都會要求把數量較大的CL分解成數量較小的幾個CL。新加入Google的SWE和SET都需要通過持續提交優秀的CL,來獲取一個“可讀性”方麵的代碼審查資格。可讀性與編程語言有關,Google內部主要的編程語言C++、Java、Python和JavaScript都有不同的可讀性要求。有經驗和值得信賴的開發人員,會得到“可讀性”的資格,大家同心協力確保整個代碼庫看起來像是由一個人編寫的一樣(注:Google的C++代碼風格指南是對外公開的,參見https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml)。

在CL提交審查之前,會經過一係列的自動化檢查。這種自動化靜態檢查所使用的規則包含一些簡單的確認,例如是否遵循Google的代碼風格指南、提交CL相關的測試用例是否執行通過(原則上所有的測試必須全部通過)等。CL裏麵一般總是包含針對這個CL的測試代碼,測試代碼總是和功能代碼在一起。在檢查完成之後,Mondrian會給相應的CL審閱者發送一封包含這個CL鏈接的通知郵件。隨後審閱者會進行代碼審查,並把修改建議發回給SWE去處理。這個過程會反複進行,直到提交者和審閱者都滿意為止。

提交隊列(譯注:submit queue)的主要功能是保持“綠色”的構建,這意味著所有測試必須全部通過。這是構建係統和版本控製係統之間的最後一道防線。通過在幹淨環境中編譯代碼並運行測試,提交隊列係統可以捕獲在開發機器上無法發現的環境錯誤,但這會導致構建失敗,甚至是導致版本控製係統中的代碼處於不可編譯的狀態。

規模較大的團隊可以利用提交隊列在同一個代碼分支上進行開發。如果沒有提交隊列,通常在代碼集成或每輪測試時都會把代碼凍結,使用提交隊列就可以避免這個問題。在這種模式下,提交隊列可以使得規模較大團隊就像小團隊一樣,高效且獨立。由於這樣增加了開發提交代碼的頻率,勢必給SET的工作帶來了較大難度,這可能是唯一的弊端。

提交隊列和持續集成構建由來

by Jeff Carollo

在Google規模還很小的初期,有一個約定的習俗就是在代碼提交之前需要運行所有已經編寫好的單元測試,用以驗證這次代碼變更的質量是否滿足要求。測試運行失敗的情況常常會發生,大家不得不花時間去找到問題的根源並加以修複。

公司在不斷變大,為了節省資源,高質量的公共基礎庫被工程師們編寫實現、維護和共用。且隨著時間的變化,這些核心公共代碼在數量上、規模上和複雜性上都有顯著的增長。在這個時候,僅僅依靠單元測試就不夠了,在一些與外部公共庫或框架有交互的地方還需要依賴集成測試的驗證。此時Google也發現許多測試運行失敗的原因都是由於其外部依賴所導致。但在沒有代碼提交之前,這些測試不會被運行,即使它們已經失敗數天之久也無人知曉。

這個時候“單元測試展板(Unit Test Dashboard)”出現了。這個係統把所有公司代碼庫的一級目錄都作為一個“項目”,當然也允許自己增加自定義的“項目”,隻要提供一係列構建和測試維護人員信息即可。這個係統會每日運行所有項目的測試。在展板上展示一個報表,記錄著每個項目的測試通過與失敗比率。每日運行失敗的項目維護者也會收到一封相應的通知郵件,雖然測試運行失敗通常不會持續太長的時間,但依然還會有失敗的情況發生。

有些團隊希望能夠盡早知道哪些代碼變更可能引起構建失敗。每24小時才運行一次所有測試已經不能滿足要求。個別團隊就開始去編寫持續構建腳本,在專用機器上持續不斷地構建並運行相應的單元測試與集成測試。後來發現這個係統具有一定的通用性,也可以用來支持其他團隊,Chris Lopez和Jay Corbett就一起編寫了“Chris/Jay持續構建”工具,其他團隊通過注冊一台機器、填寫一個配置文件和運行一個腳本,就能夠運行自己的持續集成了。這很快變成了一個標準做法,後來幾乎所有的Google項目都在使用Chris/Jay持續構建工具。在測試運行失敗之後,會給最近一次提交代碼的開發人員發送一封通知郵件,因為他們極有可能是導致測試失敗的元凶。另外,Chris/Jay持續構建工具找出了“黃金變更列表”,這些代碼變更在版本控製係統上得到確認,所有相關的測試和構建都已經成功通過。這樣開發可以得到幹淨的代碼版本而不受到最近提交代碼的影響,最近提交的代碼可能會導致構建失敗(對於挑選用於發布的版本會非常有幫助)。

還有部分團隊希望能夠更早地捕獲引起構建失敗的代碼變更。隨著項目規模和複雜度的上升,一旦發生構建失敗就已經有些晚了,就需要花費很大代價去修複。出於保護持續構建係統的目的,提交隊列就出現了。在早期實現版本中,所有等待提交的CL必須逐個排隊,等待測試,如果測試通過則證明這個CL是沒有問題的,可以提交進代碼庫(因此也需要排隊)。當有大量長時間運行的測試需要執行時,CL在發送給提交隊列和CL真正被提交到源碼庫之間可能需要消耗數小時,這確實也很常見。在後來的實現中,允許所有等待的CL在互相隔離的前提下,並發地構建並運行測試。這樣的改進可能會引起一些競爭條件的出現,但實際上很少發生,他們最終也都會被持續構建係統所捕獲。快速地提交代碼,省下的時間遠遠大於解決偶爾需要修複持續構建錯誤的時間。多數Google大型項目都在使用提交隊列,項目成員會輪流做“構建警察”,構建警察的職責是快速響應處理任何在提交隊列和持續構建係統中遇到的問題。

整套係統(單元測試展板、Chris/Jay持續構建工具和提交隊列)在Google存活了相當長的時間(數以年計)。它們隻需很少的搭建時間成本和不同程度的維護工作,但卻給團隊提供了極大的幫助。可以這樣講,它已經成為一個實用可行的公用基礎工具,為所有團隊在係統集成方麵提供幫助。測試自動化,簡寫TAP(譯注:Test Automation Program)就是這樣做的。TAP幾乎應用於所有的Google項目,但Chromium和Android除外(它們是開源項目,使用了不同代碼庫和構建環境)。

雖然所有的團隊使用相同的一套工具和基礎框架有一定的益處,但這些益處也不能被過分誇大。有些簡單的小工具也可以解決現實問題。工程師使用一個簡單的命令在雲端提交CL、並發構建、運行所有可能涉及的測試代碼,並將運行結果可視化地展示在一個永久的網站上。在命令運行終端也會顯示“成功”、“失敗”,以及指向任務詳情的超鏈接。如果開發選擇使用這樣的方式,他的測試結果(包括覆蓋率信息)就會被存儲在雲端,並通過Google內部代碼審查工具對所有的代碼審查者可見。

2.1.9 SET的工作流程:一個實例
現在讓我們把所有與SET相關的東西拚裝在一起,看一個完整的實例。需要注意的是,這部分將涉及部分技術內容,且會深入到某些底層細節裏麵。如果你隻對SET概要介紹感興趣,那麼你可以跳過這一部分。

假設有一個簡單的網絡應用,它的功能是允許用戶向Google提交URL,並把這個URL增加到Google的索引文件之中。HTML的網頁表單頁麵上接收兩個字段:url和相應的注釋,然後向Google的服務器發送類似以下的一個HTTP GET請求。

GET /addurl?url=https://www.foo.com&comment=Foo+comment HTTP/1.1
在這個例子中,這個web應用的服務器端分成至少兩部分:前端服務AddUrlFrontend(它接收原始的HTTP請求,並做解析和驗證工作)和後端服務AddUrlService。這個後端服務接受來自於前端服務AddUrlFrontend的請求,檢查數據是否有錯,並與後端數據存儲持久層(例如Google的Bigtable(譯注:https://labs.google.com/papers/bigtable.html)或GFS Goolge文件係統(譯注:https://labs.google. com/papers/gfs.html)進行交互。

SWE針對這個服務,要做的第一件事就是為這個項目創建一個目錄。

$ mkdir depot/addurl/
他們使用Google Protocol Buffer描述性語言(注:https://code.google.com/apis/protocol buffers/ docs/overview.html)定義AddUrlService的協議。

File: depot/addurl/addurl.proto
message AddUrlRequest {
required string url = 1;  // The URL entered by the user.
optional string comment = 2; // Comments made by the user.
}
message AddUrlReply {
// Error code, if an error occurred.
optional int32 error_code = 1;
// Error message, if an error occurred.
optional string error_details = 2;
}
service AddUrlService {
// Accepts a URL for submission to the index.
rpc AddUrl(AddUrlRequest) returns (AddUrlReply) {
option deadline = 10.0;
}
}

上麵的“addurl.proto”文件定義了三個重要部分:AddUrlRequest的消息格式AddUrlReply的消息格式、AddUrlService遠程方法調用服務(RPC)。

通過查看AddUrlRequest消息的定義,我們可以知道調用者必須提供一個url字段,而另外一個comment字段是可選的。

類似地,通過檢查AddUrlReply消息的定義,我們可以知道error_code 和 error_details兩個服務器提供的響應字段都是可選的。我們可以安全地假設:當一個URL被成功接收以後這些字段一般情況下會返回為空,這樣也可以最小化中間的數據傳輸量。這是Google的慣例,讓常見的場景快速運行。

通過查看AddUrlService服務的定義可以知道單一服務方法——AddUrl,接受一個AddUrlRequest並返回一個AddUrlReply。默認情況下,如果client在調用AddUrl 之後10秒還沒有收到任何回應就會超時。AddUrlService在實現上會與後端持久數據存儲層再做交互,但client並不需要關心這一部分細節,所以在“addurl.proto”文件中沒有這部分接口的定義詳情。

在消息字段中出現的“=1”並不是指這個字段的值。這種使用方法是為了允許協議將來升級使用。例如,以後某人可能想增加一個額外的uri字段到AddUrlRequest消息中。為了實現這個,他們可以做如下變更。

message AddUrlRequest {
required string url = 1;  // The URL entered by the user. 
optional string comment = 2; // Comments made by the user. 
optional string uri = 3;  // The URI entered by the user.
}

但這樣做會有點傻。一些人更希望直接把url字段修改為uri。如果使用相同的數值,老版本和新版本之間就會保持兼容性。

message AddUrlRequest {
required string uri = 1;  // The URI entered by the user.
optional string comment = 2; // Comments made by the user.
}
在完成addurl.proto以後,開發人員可以為proto_library創建構建規則,根據addurl.proto中定義的字段自動產生C++源文件並編譯成一個C++靜態庫(增加額外的選項,也可以綁定到其他語言,如Java或Ptyhon)。

File: depot/addurl/BUILD
proto_library(name=”addurl”,
srcs=[“addurl.proto”])

開發人員使用構建係統,並修複在構建過程中可能出現的addurl.proto問題或構建定義文件中的問題。構建係統會調用Protocol Buffer編譯器,產生源碼文件addurl.pb.h和addurl.pb.cc,同時會產生一個可以被鏈接的靜態庫adurl。

現在可以新建文件addurl_frontend.h,並在其中定義AddUrlFrontend類。代碼大體如下。

File: depot/addurl/addurl_frontend.h
#ifndef ADDURL_ADDURL_FRONTEND_H_
#define ADDURL_ADDURL_FRONTEND_H_
// Forward-declaration of dependencies.
class AddUrlService;
class HTTPRequest;
class HTTPReply;
// Frontend for the AddUrl system.
// Accepts HTTP requests from web clients,
// and forwards well-formed requests to the backend.
class AddUrlFrontend {
public:
// Constructor which enables injection of an
// AddUrlService dependency.
explicit AddUrlFrontend(AddUrlService* add_url_service);
~AddUrlFrontend();
// Method invoked by our HTTP server when a request arrives
// for the /addurl resource.
void HandleAddUrlFrontendRequest(const HTTPRequest* http_request,
HTTPReply* http_reply);
private:
AddUrlService* add_url_service_;
// Declare copy constructor and operator= private to prohibit
// unintentional copying of instances of this class.
AddUrlFrontend(const AddUrlFrontend&);
AddUrlFrontend& operator=(const AddUrlFrontend& rhs);
};
#endif // ADDURL_ADDURL_FRONTEND_H_

繼續AddUrlFrontend類的實現部分,開發人員創建“addurl_frontend.cc”文件。這是AddUrlFrontend類的主要邏輯實現部分,為了簡短說明,省略了部分文件內容。

File: depot/addurl/addurl_frontend.cc
#include “addurl/addurl_frontend.h”
#include “addurl/addurl.pb.h”
#include “path/to/httpqueryparams.h”
// Functions used by HandleAddUrlFrontendRequest() below, but
// whose definitions are omitted for brevity.
void ExtractHttpQueryParams(const HTTPRequest* http_request,
HTTPQueryParams* query_params);
void WriteHttp200Reply(HTTPReply* reply);
void WriteHttpReplyWithErrorDetails(
HTTPReply* http_reply, const AddUrlReply& add_url_reply);
// AddUrlFrontend constructor that injects the AddUrlService
// dependency.
AddUrlFrontend::AddUrlFrontend(AddUrlService* add_url_service)
: add_url_service_(add_url_service) {
}
// AddUrlFrontend destructor - there’s nothing to do here.
AddUrlFrontend::~AddUrlFrontend() {
}
// HandleAddUrlFrontendRequest:
// Handles requests to /addurl by parsing the request,
// dispatching a backend request to an AddUrlService backend,
// and transforming the backend reply into an appropriate
// HTTP reply.
//
// Args:
// http_request - The raw HTTP request received by the server.
// http_reply - The raw HTTP reply to send in response.
void AddUrlFrontend::HandleAddUrlFrontendRequest(
const HTTPRequest* http_request, HTTPReply* http_reply) {
// Extract the query parameters from the raw HTTP request.
HTTPQueryParams query_params;
ExtractHttpQueryParams(http_request, &query_params);
// Get the ‘url’ and ‘comment’ query components.
// Default each to an empty string if they were not present
// in http_request.
string url = query_params.GetQueryComponentDefault(“url”, “”);
string comment = query_params.GetQueryComponentDefault(“comment”, “”);
// Prepare the request to the AddUrlService backend.
AddUrlRequest add_url_request;
AddUrlReply add_url_reply;
add_url_request.set_url(url);
if (!comment.empty()) {
add_url_request.set_comment(comment);
}
// Issue the request to the AddUrlService backend.
RPC rpc;
add_url_service_->AddUrl(
&rpc, &add_url_request, &add_url_reply);
// Block until the reply is received from the
// AddUrlService backend.
rpc.Wait();
// Handle errors, if any:
if (add_url_reply.has_error_code()) {
WriteHttpReplyWithErrorDetails(http_reply, add_url_reply);
} else {
// No errors. Send HTTP 200 OK response to client.
WriteHttp200Reply(http_reply);
}
}
HandleAddUrlFrontendRequest是一個經常被調用的成員函數。許多Web處理函數大多如此。開發人員可以通過提取一些功能到helper函數中,用來簡化這個函數。但是,類似這樣的重構在構建穩定之前和單元測試編寫完成並可以順利通過運行之前是很少去做的。

在這個時候,開發人員修改已有addurl項目的構建文件,為addurl_frontend庫增加入口。在構建的時候會產生一個C++靜態庫AddUrlFrontend。

```javascript
File: /depot/addurl/BUILD
# From before:
proto_library(name=”addurl”,
srcs=[“addurl.proto”])
# New:
cc_library(name=”addurl_frontend”,
srcs=[“addurl_frontend.cc”],
deps=[
“path/to/httpqueryparams”,
“other_http_server_stuff”,
“:addurl”, # Link against the addurl library above.
])

再次運行構建工具,同時修複在編譯鏈接addurl_frontend.h和addurl_frontend.cc過程中可能出現的錯誤,直到所有編譯和鏈接不出現警告和錯誤為止。此時,可以去編寫AddUrlFrontend的單元測試代碼了。單元測試在另外一個新文件“addurl_frontend_test.cc”中。在測試中定義一個虛假(fake)的後端服務,使用AddUrlFrontend的構造函數可以把這個虛假的後端服務在運行時刻調用。這樣的話,單元測試在運行時,無需修改AddUrlFrontend代碼本身,代碼邏輯能夠進入AddUrlFrontend內部期望分支中或錯誤流程裏(譯注:閱讀以下代碼需要提前了解Google’s framework for writing C++ test,即googletest,參見https://code.google.com/p/googletest/)。

File: depot/addurl/addurl_frontend_test.cc
#include “addurl/addurl.pb.h”
#include “addurl/addurl_frontend.h”
// See https://code.google.com/p/googletest/
#include “path/to/googletest.h”
// Defines a fake AddUrlService, which will be injected by
// the AddUrlFrontendTest test fixture into AddUrlFrontend
// instances under test.
class FakeAddUrlService : public AddUrlService {
public:
FakeAddUrlService()
: has_request_expectations_(false),
error_code_(0) {
}
// Allows tests to set expectations on requests.
void set_expected_url(const string& url) {
expected_url_ = url;
has_request_expectations_ = true;
}
void set_expected_comment(const string& comment) {
expected_comment_ = comment;
has_request_expectations_ = true;
}
// Allows for injection of errors by tests.
void set_error_code(int error_code) {
error_code_ = error_code;
}
void set_error_details(const string& error_details) {
error_details_ = error_details;
}
// Overrides of the AddUrlService::AddUrl method generated from
// service definition in addurl.proto by the Protocol Buffer
// compiler.
virtual void AddUrl(RPC* rpc,
const AddUrlRequest* request,
AddUrlReply* reply) {
// Enforce expectations on request (if present).
if (has_request_expectations_) {
EXPECT_EQ(expected_url_, request->url());
EXPECT_EQ(expected_comment_, request->comment());
}
// Inject errors specified in the set_* methods above if present.
if (error_code_ != 0 || !error_details_.empty()) {
reply->set_error_code(error_code_);
reply->set_error_details(error_details_);
}
}
private:
// Expected request information.
// Clients set using set_expected_* methods.
string expected_url_;
string expected_comment_;
bool has_request_expectations_;
// Injected error information.
// Clients set using set_* methods above.
int error_code_;
string error_details_;
};
// The test fixture for AddUrlFrontend. It is code shared by the
// TEST_F test definitions below. For every test using this
// fixture, the fixture will create a FakeAddUrlService, an
// AddUrlFrontend, and inject the FakeAddUrlService into that
// AddUrlFrontend. Tests will have access to both of these
// objects at runtime.
class AddurlFrontendTest : public ::testing::Test {
protected:
// Runs before every test method is executed.
virtual void SetUp() {
// Create a FakeAddUrlService for injection.
fake_add_url_service_.reset(new FakeAddUrlService);
// Create an AddUrlFrontend and inject our FakeAddUrlService
// into it.
add_url_frontend_.reset(
new AddUrlFrontend(fake_add_url_service_.get()));
}
scoped_ptr<FakeAddUrlService> fake_add_url_service_;
scoped_ptr<AddUrlFrontend> add_url_frontend_;
};
// Test that AddurlFrontendTest::SetUp works.
TEST_F(AddurlFrontendTest, FixtureTest) {
// AddurlFrontendTest::SetUp was invoked by this point.
}
// Test that AddUrlFrontend parses URLs correctly from its
// query parameters.
TEST_F(AddurlFrontendTest, ParsesUrlCorrectly) {
HTTPRequest http_request;
HTTPReply http_reply;
// Configure the request to go to the /addurl resource and
// to contain a ‘url’ query parameter.
http_request.set_text(
“GET /addurl?url=https://www.foo.com HTTP/1.1\r\n\r\n”);
// Tell the FakeAddUrlService to expect to receive a URL
// of ‘https://www.foo.com’.
fake_add_url_service_->set_expected_url(“https://www.foo.com”);
// Send the request to AddUrlFrontend, which should dispatch
// a request to the FakeAddUrlService.
add_url_frontend_->HandleAddUrlFrontendRequest(
&http_request, &http_reply);
// Validate the response.
EXPECT_STREQ(“200 OK”, http_reply.text());
}
// Test that AddUrlFrontend parses comments correctly from its
// query parameters.
TEST_F(AddurlFrontendTest, ParsesCommentCorrectly) {
HTTPRequest http_request;
HTTPReply http_reply;
// Configure the request to go to the /addurl resource and
// to contain a ‘url’ query parameter and to also contain
// a ‘comment’ query parameter that contains the
// url-encoded query string ‘Test comment’.
http_request.set_text(“GET /addurl?url=https://www.foo.com”
“&comment=Test+comment HTTP/1.1\r\n\r\n”);
// Tell the FakeAddUrlService to expect to receive a URL
// of ‘https://www.foo.com’ again.
fake_add_url_service_->set_expected_url(“https://www.foo.com”);
// Tell the FakeAddUrlService to also expect to receive a
// comment of ‘Test comment’ this time.
fake_add_url_service_->set_expected_comment(“Test comment”);
// Send the request to AddUrlFrontend, which should dispatch
// a request to the FakeAddUrlService.
add_url_frontend_->HandleAddUrlFrontendRequest(
&http_request, &http_reply);
// Validate that the response received is a ‘200 OK’ response.
EXPECT_STREQ(“200 OK”, http_reply.text());
}
// Test that AddUrlFrontend sends proper error information when
// the AddUrlService encounters a client error.
TEST_F(AddurlFrontendTest, HandlesBackendClientErrors) {
HTTPRequest http_request;
HTTPReply http_reply;
// Configure the request to go to the /addurl resource.
http_request.set_text(“GET /addurl HTTP/1.1\r\n\r\n”);
// Configure the FakeAddUrlService to inject a client error with
// error_code 400 and error_details of ‘Client Error’.
fake_add_url_service_->set_error_code(400);
fake_add_url_service_->set_error_details(“Client Error”);
// Send the request to AddUrlFrontend, which should dispatch
// a request to the FakeAddUrlService.
add_url_frontend_->HandleAddUrlFrontendRequest(
&http_request, &http_reply);
// Validate that the response contained a 400 client error.
EXPECT_STREQ(“400\r\nError Details: Client Error”,
http_reply.text());
}

通常情況下開發人員會寫更多的測試用例,但這裏隻是通過上麵的示例來演示通用模式,即如何定義Fake對象、如何注入這個Faoke對象、在測試中如何調用這個Fake對象來引入期待的錯誤並驗證程序邏輯,上麵的例子就已經足夠了。有一個需要注意的地方,那就是此例中我們缺少了模擬AddUrlFrontend和FakeAddUrlService之間的網絡超時。這說明我們的開發人員忘記了去處理在超時條件下的檢查驗證邏輯。

有經驗的敏捷測試高手會指出所有測試使用FakeAddUrlService有點單一,也可以使用mock來替換。這個高手

最後更新:2017-06-06 10:31:29

  上一篇:go  從ConcurrentHashMap的演進看Java多線程核心技術
  下一篇:go  阿裏內核月報2014年4月