用有效的測試培養工程——《Growing Object-Oriented Software, Guided by Tests》讀後感

  這本書2009年10月就出來了,當時沒來得及細看,只是把它放入了我的待讀列表中。後來查到2010年8月也出了中文版,書名叫《測試驅動的面向對象軟件開發》。看完全書後,我發現本書重點談的還是軟件培養問題。Growing這個詞出現在書的標題中,非常吸引我的思路。
 
  在前言中,作者開宗明義,講了本書要強調的五個問題:1.如何讓測試驅動開發適應我的工程;2.從那裏開始做TDD;3.如何寫單元測試與端對端測試;4.測試驅動開發的“驅動”是何意;5.如何測試某一個困難的功能。
 
  第一部分是簡介。導言部分說了,TDD不僅是XP(極限編程)的核心實踐,而且也被Crystal Clear,Scrum等敏捷開發方法才用。敏捷和非敏捷項目都可應用TDD,甚至是純研究項目都可以。
 
  第一章重申TDD的紀律——“在沒有一個失敗的用例之前,不要寫任何功能”。其後提出,將傳統的“測試-實現-重構”小迭代外面,包上一個驗收測試(Acceptance Test)。驗收測試是測試整個系統是否能工作的。與之區別的是用於測試現有代碼與我們不能改變其代碼的模塊是否能配合工作的集成測試(Integration Test),以及測試對象本身功能的單元測試(Unit Test)。最後強調了單元測試能夠給我們一個發現不良設計以及重構代碼的機會。
 
  接下來第二章講述了“值類”和“對象類”的區別。針對不變的數量和度量進行建模,就是“值類”,它有些類似“不可變類”(Immutable Class)或者無狀態(Stateless)類。如果針對行爲進行建模,則是“對象類”了。最後講述了要使用單一的“告知型”調用代替請求式的一連串調用,例如用“master.allowSavingOfCustomisations();”來代替“((EditSaveCustomizer)master.getModelisable().getDockablePanel().getCustomizer()).getSaveItem().setEnabled(Boolean.FALSE.booleanValue());"。第三章介紹了一些TDD的基本工具。
 
  第二部分講述了TDD的具體過程。第四章講了如何確定測試驅動開發的第一個迭代週期。提出先實現最少量的真實功能以便可以自動地構建、部署、端對端測試。這叫做“行走骨架”(Walking Skeleton),具體的說,是通過“理解問題->設計草圖->自動化構建,部署,端對端測試->普通迭代”這個流程來做的。其後說道,測試先行的工程早起會引入混亂,但是迅速降低,因爲找到了發展的方向,預見了可能發生的錯誤。相反,測試後行(或到發佈期限前才集成)的工程在最後會出現大量的混亂。這可以作爲是否選取TDD的一個參考標準。而“行走骨架”的好處就是,能讓我們在仍有時間、預算和解決問題的意願時,去解決問題,而不至於到了發佈前才發現工程已經失控。
 
  第五章講了如何維護測試驅動週期。作者提出,將衡量進度的測試(針對新功能的驗收測試)和用以捕捉“功能破壞”(Regression)的測試(已有的驗收測試、整合測試、單元測試)分隔開來。“筆者注:廣義的迴歸測試即爲了保證正在開發的新功能不破壞既有功能而寫的測試,狹義的迴歸測試專指前述的驗收測試。”此後提到兩個問題,一是不要着急寫單元測試,以免整合時發現功能不符合需要,二是要學會“傾聽測試”——難於測試的功能往往意味着設計需要改進。如不改進,如果功能增多,則該有問題的設計會更難於修改。
 
  第六章講述瞭如何在TDD中使用面向對象的風格。作者講述了Cockburn的“端口與適配器”架構,業務領域的代碼應該同技術設施,例如數據庫,UI等,分離開來。不要讓技術概念泄漏入應用程序模型。所以要通過接口將應用程序核心業務與每個技術領域橋接起來。這就是Eric Evans在《領域驅動設計》中所說的防腐層(Anticorruption Layer)。其後又講了封裝和信息遮掩的區別。封裝主要是相對於“對端”(Peer)來說的,強調訪問只能通過API進行。而信息遮掩主要是針對上層業務來說的,意在使得高層邏輯不需關注低層細節。封裝和信息遮掩中的兩個常見問題即是通過API返回內部實現而產生的“別名”效果。以及沒能提供直接的API調用,導致客戶代碼通過自己組合API來完成任務,就像前述的一連串調用那樣,暴露了過多細節。接下來講了達成“單一責任原則”的一個口訣:不用任何連詞(和,或,但)去描述對象。如果有從句,那麼應該按照從句,把大對象拆分成一個個互相合作的小對象。這對於我們檢視自己的設計很有幫助。作者將設計中一個對象的協作者,分爲三種角色模板。即依賴其服務方能運轉的“依賴物”(Dependencies),用於通知事件而不關注其具體身份的“被通知物”(Notifications),以及利用其調整自身行爲以符合系統需求之“調整物”(Adjustments)。其後作者講了“組合體對象”(筆者注:即組合各種對象來完成自身任務的功能模組,不同於設計模式中的組合體模式)所提供的API要比其各自部分的API總和簡單。它封裝了組件的存在及其內部互動,爲其對端展示出一個更簡單的抽象接口。本章的最後講述了“環境獨立性”的重要。“環境獨立性”規則幫我們判斷一個對象是否隱藏的太多或者隱藏了錯誤的信息。當執行環境變化時,環境獨立的對象是易於改變的。其運行大環境可以通過構造器(如果對於該對象是貫穿生命期的)或需要環境的方法(如果是瞬態的)傳入。
 
  第七章繼續講述如何達成面向對象的設計。首先講述TDD對OO設計的幫助:1.我們必須先描述我們要做什麼,而後纔是怎麼做;2.爲了使得單元測試易懂(由此變得可維護),我們必須限定它們的範圍(筆者注:如果單元測試過長或者setup階段太繁複,則意味着受測的那個大對象需要拆解);3.我們必須將其依賴物傳給它,這意味着我們必須知道它依賴的都是些什麼東西。再說了接口和協議的作用:接口描述了兩個組件是否互相適配,而協議則描述了他們是否能一起運轉。又講了測試可以幫助我們發現設計中的問題:一個雜亂或不清晰的測試暗示着我們暴露了太多實現,而且我們應該重新考量該對象及其臨接物件的責任分配。在講到值類和對象類的設計時,作者提出了三個技巧:打散(將一個大對象分割成一組互相協作的小對象)、剝離(定義一個對象所需的新服務,增加一個提供該服務的新對象)、捆綁(將相關對象藏入一個容器對象中)。最後在談到接口問題時,作者提出了兩個觀點。一個說道:針對某一個接口的實現而寫的Impl類是沒有意義的。如果實現類真的沒有一個好名字,那可能意味着接口的命名或者設計很糟。可能它因爲有太多的責任而喪失了重點;也可能它是以實現的角度命名,而非以其在客戶代碼的角度上;又或者它是一個值而非一個實體對象——這種不協調有時會在寫單元測試時呈現出來。(例如MyInteger和MyIntegerImpl這種接口分離就是很糟糕的)另一個說:應該根據需要合併或者拆分接口。在發現實現類的結構不清晰時,應考慮接口是否沒有側重點,需要拆解。
 
  第八章講如何在第三方代碼之上構建自己的工程。作者建議寫一個適配器對象層,其使用第三方API實現它們的接口。我們用有重點的集成測試去測試這些適配器,以確認我們理解了第三方API是如何工作的。
 
  第三部分講了一個例子,用開發一個捕捉拍賣行情而自動出價的競拍器,來說明如何以測試爲指導,去培養OO軟件開發。本部分跨越了十一章。在這個過程中,穿插着對前兩章所講原則的實例化運用。第十一章演示瞭如何用最少的代碼搭建起來可以執行的端對端測試。在本例中,僅有一個測試用空殼服務器,一個Swing窗口(最少的代碼),主程序向“服務器”發送加入消息,覈實服務器確實收到消息;服務器關閉競拍,窗口顯示失敗消息,覈實窗口確實顯示了失敗消息(可執行的端對端測試)。第十二章對如何組織測試提出了小建議:將測試放在一個不同的包中。防止通過包級別的後門去測試,同時方便在IDE中瀏覽。第十五章講述了修改命名的重要性:重命名代碼中的若干功能,這是開發進程的一個重要部分,就像我們可以用已經寫出的代碼來更好地理解結構應該如何發展一樣,我們也通過用已經修改過的名字去編程以更好地理解這些名字的意義。我們可以理解類型和方法名是如何互相配合起來工作的,以及概念是否清晰,這都會激發我們發現新的想法。如果一個功能的名字不對,唯一能做的明智事情就是改變它,以免過後閱讀代碼的人花了數不清的時間也弄不清代碼在幹什麼。第十七章說到靜態設計和動態設計的問題:重構非常關注於靜態結構(類與接口),以至於很容易忽略應用程序動態結構(實例與線程)。有時我們需要退一步考慮,去畫一個類似互動圖那樣的東西。其後的第十八章說道TDD應該和其它的建模手段結合起來使用,並且不要把其它的建模技巧當作一種目的,而是要理解它們,並且把它們當作支持與指導開發的一種手段。最後第十九章說我們必須知道如何漸進式的改變代碼,尤其是使得代碼結構良好,以至於我們可以根據需求的改變把代碼帶我們想去的地方。
 
  第四部分標題“可持續的測試驅動開發”,意爲教大家如何提高測試的質量,以便讓測試能夠更及時、更好地提供關於設計缺陷的反饋。第二十章講了幾個知識點。當我們在測試驅動開發的過程中提取一個接口,我們就必須想出一個名字來描述剛剛發現的關係。我們覺得這使我們深入思考領域問題,並梳理出可能會錯過的概念。只覆寫可見的方法(即保護的和公有的)。這個規則防止了僅僅爲了測試能覆寫而暴露了內部方法。在談到模仿對象(Mock Object)時,作者說有兩個試探法可以決定一個類是不是像值類從而不值得去仿造它。第一,它的值是不可變的;第二,我們想不出來一個有意義的名字來描述把這個類當作接口之後,實現它的那個類。在對付膨脹的(參數過多的,過於複雜的)構造器時,作者提出可以提取出隱含的組件。其要尋找兩個條件:經常在類裏一起使用的參數;擁有相同生命期的參數。當我們剛好找到了這種情況時,就得努力想出一個好名字來解釋這個概念。一個做得好的設計,其標誌之一即是這種改變可以很容易集成進來。我們堅持依賴物應該通過構造器傳入,但是被通知物和調整物可以設置爲缺省值,稍後再行配置。當一個構造器太大了,並且我們不認爲參數中隱含有一個新的類型時,我們可以用更多的缺省值,僅在有特殊的測試用例時才覆蓋掉它們。在談到期望陳述時,作者說要避免太多的期望。如果我們有很多期望,要麼就是視圖測試一個過於龐大的單位,要麼就是鎖定了太多的對象交流行爲。作者還談到了傾聽測試給我們帶來的四個好處:使領域知識局部化;將對象間的關係抽象並命名成類;通過定義類型和角色而帶來更多的名字,就意味着帶來更多的領域信息;與其提供數據,不如提供行爲。本章最後總結說:測試驅動開發是低容忍度的。低質量的測試會使得開發速度變得非常慢,而且受測系統內部代碼質量低的話,會導致測試的質量也跟着變低。
 
  第二十一章講了測試的可讀性問題。以下五種情況應當改進:1.測試名稱沒有清晰的描述出每個測試用例的重點,以及它和其餘測試用例的差別;單一的測試用例執行了多個功能;測試用例間的結構差異很大,以至於讀者不能通過速讀來理解它們的意圖;過多的設置和異常處理代碼,將業務邏輯淹沒其中;測試使用了不明其意的字面值(魔法數字)。在講到如何命名測試時,作者介紹了TestDox約定法,即使每一個名字讀起來像一個句子,其隱含主語即爲測試目標,例如:A List holds items in the order they were added. A List can hold multiple references to the same item. A List throws an exception when removing an item it doesn’t hold.即可翻譯成三個測試方法的名稱:holdsItemsInTheOrderTheyWereAdded(),canHoldMultipleReferencesToTheSameItem(),throwsAnExceptionWhenRemovingAnItemItDoesntHold()。在變量的命名上,作者強調我們應該用能夠顯示這些值或者對象在測試中所扮演的角色以及他們同目標對象的關係的名字來命名。
 
  第二十二章講了如何構建複雜的測試數據。測試數據構建器的一個好處是,我們可以寫出易於閱讀且便於發現錯誤的測試代碼,因爲每個構建器方法都指明瞭它的參數的意圖。
 
  第二十三章講述瞭如何從測試失敗信息中演進工程代碼。作者說,就算是發生在和我們現在所做無關的領域裏面,未預期的測試失敗,也可能是有價值的。因爲它們揭示了代碼中我們所未注意到的隱含關係。如果一個失敗的測試清楚的解釋了失敗的東西和原因,我們就可以快速的排查並修正代碼。同時作者建議,“經常同源代碼庫同步——可以到隔幾分鐘就一次的頻度——以便一旦一個測試突然失敗了,你不需要花費多長時間就能撤銷最近的修改,並去嘗試另一個方法。……比起一直查錯,有時候回滾代碼並以一個清晰的頭腦重新思考“如何去寫”,可能會更快。”
 
  第二十四章講了測試的靈活性。如果一個對象因爲有太多的依賴物或者其依賴物是隱藏的,從而很難從它的環境中解耦,那麼當系統的某個偏遠角落改變了,測試就會失敗。
 
  最後的第五部分講述了一些高級話題,第二十五章講述了持久化。作者提出將影響持久化狀態的測試孤立開來。將執行持久話操作的測試和針對被持久話對象進行的測試分開來做。並且提及一個小技巧:不要以模式來命名類或者接口,它們與系統其它類之間的關係纔是重要的。當它們的工作方式改變時,這樣做會使得名稱具有誤導性。第二十六章講述了單元測試和線程。作者提出,併發是一個系統級別的關注點,我們應該在需要執行併發任務的對象“功能對象”之外來操控它。最後第二十七章講述了測試異步代碼的問題。作者指出,一個測試可以有兩種方式觀察系統:採樣可被觀測的狀態或者監聽它發出的事件。同時還指出了異步測試的一個注意點:測試可能會在系統之前運行以至於沒有測試任何有用的東西。這會造成貌似正確的結果:錯誤的代碼看起來好像能正常運行。還提出,採樣測試與監聽事件測試的一個明顯區別是,輪詢可能會錯過被新近狀態所覆蓋掉的狀態更新。解決辦法是,可以查找記錄。觸發一個刺激事件,並等系統狀態穩定再查詢。作者又提出,我們經常採用一個命名方案去區分同步與斷言。例如waitUntil()是等待某一個受測系統穩定(同步代碼),而assertEventually()則是斷言某個事件最終會發生。本章最後作者講述了測試排期事件的問題。通過將事件排期機制從系統中解耦,可以使得系統的行爲具有確定性從而更易測試事件。我們可以將事件的生成抽取成一個由外部驅動的共享服務。
 
  本書的跋很值得一讀。寫了整個jMock從構想,初創,演進到鞏固的過程。起初是爲了方便測試某一個對象內部的功能機制是否如我們預期,後來逐漸把關注點從參數的值轉移到了對象之間的信息溝通上。現在jMock已經成爲一個單元測試和驗收測試中進行期望陳述與斷言的常用庫了。讀者有必要熟悉並掌握它。最後的兩個附錄講了jMock2庫和Hamcrest匹配器的使用方法,如果對書前面的範例代碼中的用法不太熟悉,可以參考。
 
  總的說來,在讀此書的過程中我非常驚喜,發現儘管TDD有很多令人詬病的缺點,但是仍然有人和我想像一樣,用不瘟不火的穩健心態來創造性的加入“培養”要素,以使得TDD對工程開發有更大的促進作用。測試是一個良好的反饋來源,可以真實的反映出我們在設計中考慮不周到之處,以及時督促我們改進產品代碼的設計。要想讓測試能夠如此培養軟件的開發,就必須着力於測試代碼的先行性、正確性、可讀性與靈活性。同時要注意用驗收測試來催生新的迭代週期,在修改代碼時不僅要運行單元測試,更要及時運行驗收測試以獲得更多回饋。我想信,用心於測試的努力,必能在產品代碼的研發中產生加倍的回報。
 
本文爲原創,如需轉載請聯繫作者(Email [email protected] 微博 http://weibo.com/eastarlee
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章