Bug終結之路 --《有效的單元測試》讀後感

最近一段時間,看了《有效的單元測試》(Effective Unit Testing)這本書。這是一本對單元測試從重要性到該如何編寫和構建都進行了詳細闡述的技術書籍,其作者Lasse Koskela,是一名資深敏捷技術實踐專家、敏捷教練、培訓師、顧問和程序員,具有數十年計算機程序設計和開發經驗。該書雖沒有如《測試驅動開發》這樣的名氣,但作者在書中總結的一套理論和實踐框架,大多都非常實用,能結合到我們日常開發過程中。
單元測試有什麼好處,我想對於有一定開發經驗的程序員來說,都能說上一通,但應該怎麼做才能高效的完成單元測試卻並不是所有開發者能隨便總結的出來。本文就書中提出的方法論中能結合到我司的部分做一些簡單的總結。

一、好的單元測試標準

任何需要衡量的事物,都需要有一套標準,引用書中對於“好的單元測試”標準如下:

  1. 可讀性,與寫功能代碼一樣,寫測試代碼,包括其註釋,都需要遵守“容易讓人理解”這一“基本”要求(雖然這一要求實際還是很有難度的)。可讀性對於單元測試來說,還更賦予了“可維護性”的效果,因爲通常單元測試很多時候不需要深入到底層,所以往往可以寫得較爲簡潔,此時可讀性和可維護性就成正比了。
  2. 結構化,能有效幫助表達測試內容,即增強可讀性。書中結構化的闡述實際也是告訴我們在編寫單元測試時,應該運用所有軟件設計中的知識和技能,從設計模式甚至到微服務架構,只要是正確的設計手段就應該使用。
  3. 單元測試的名稱須名副其實,不應使用錯誤命名。這個也比較好理解,當測試用例很多,遇到沒跑通的測試用例時,如果單元測試名稱起的好,一眼就能定位到程序的問題所在,而無需一步步debug,大大節省了調試時間。
  4. 單元測試應能獨立執行,即其執行不依賴於其他單元測試的結果,這樣能更易於調試和找出程序問題。
  5. 可靠性,有用且可靠的單元測試纔是真的可靠。首先,單元測試必須是對關鍵部分進行的測試,而不是爲了通過測試而測試。如下圖,作者總結就是“從不失敗或經常失敗”的單元測試都幾乎沒有價值。其中"從不失敗”的測試被稱爲”Happy tests”,比如 “1+1=2”這種常識都知道必然正確的情況就沒必要測試;而“經常失敗”的測試則被稱爲“Random tests”,是指那些測試結果會隨着每次執行的環境或其他條件而變化的測試。
    在這裏插入圖片描述
  6. 單元測試應使用主流的標準工具,比如對於Java語言,一般都是使用Junit套件。這樣除了方便寫單元測試,也方便複用與交流。

二、測試替身(Test Double)

編寫單元測試,最常用的方法就是引入測試替身(Test Double)。Test Double 沒有找到權威的中文翻譯,暫且以我自己的理解來說明一下。實際就是爲了使測試更具有獨立性,通過Mock之類的“替身”代替被測試對象中,需要關聯的其他對象。這塊相信對使用過Mockito / PowerMock 之類工具包的同學都不會陌生。

  1. 使用測試替身,會有許多的好處:

    • 可以隔離具有相互關聯的測試對象,使單元測試更獨立。
    • 可以加速單元測試的執行,因其不用理會關聯對象的初始化等繁瑣操作。
    • 可以使被測試對象執行行爲更確定。
    • 可以方便模擬特定場景的測試用例。
    • 可以使被測試對象的內部隱藏信息更公開化,即通過自定義“替身”注入到被測試對象中,從而通過該替身獲取被測試對象作用於其上的行爲結果,以達到測試對象行爲的目的。
  2. 測試替身的類型主要分爲:

    • Test Stub: 測試存根,其目的是通過儘可能簡單的實現來支持真正的實現,其所有方法都是單行的,並返回適當的默認值。其返回的可以是硬編碼值,也可以實例化自身變體以返回不同的值以模擬不同的場景。
    • Fake Object: 僞造對象,與測試存根的區別在於,其更像是真實對象的經過優化後的精簡版本,其可以複製真實對象的行爲,但沒有使用該真實對象的副作用和其他後果。
    • Test Spy: 測試間諜,我的理解是包括了攔截、植入、竊取等行爲的綜合,是讓被測試對象在無感知的情況下按照其自身原有行爲執行,而間諜對象卻可記錄下被測試對象的行爲內容,必要時還能更換攔截點的內容,以改變其行爲。與前兩個不同之處在於,間諜代碼不需要我們實現,一般是測試工具包自動生成,這就有利於我們快速完成測試用例的開發。
    • Mock Object: 模擬對象,是一種特殊的測試間諜。其配置爲在特定情況下以特定方式運行,通過該對象通常能準確控制被測試對象的行爲方向,從而驗證其行爲是否符合預期。與Test Spy一樣,對象的代碼無需我們編寫,我們僅需要設定的特殊值,所以通常使用這種方式能高效的實現測試用例,且因爲其輕量級的特點通常還能獲得更高的執行效率。
  3. 使用測試替身的準則:

    • 根據每個單元測試的特點,選擇正確的替身類型。
    • 每個單元測試,按照 arrange,act,assert 這幾塊進行編寫。其中arrange可以認爲是準備(安排)階段,即對於測試內容的預先設置或初始化。act代表執行階段,即調用被測試對象的主要方法。assert(斷言)就是最後的驗證階段,檢驗被測試對象執行後得到的結果是否於預期的結果一致。
    • 對單元測試檢驗其行爲和結果而不應檢驗其實現細節。其實質是指對一個測試應僅測試一件事,所以必須要有重點,而不應被其複雜的內部細節左右,變成爲了測試諸多細節而導致整個單元測試變得複雜難控。
    • 選好測試工具,這個就無需贅述,常用的一般是Mockito、PowerMock、EasyMock 、JMock等。
    • 善於使用注入依賴。爲不破壞被測試對象的結構(假設該對象具有較好的實現結構),應使用注入依賴方式,這對於熟悉Mockito等工具的同學來說不是什麼難事。

三、系統的可測試性設計

對單元測試有了衡量標準,以及實現的方法,爲了能實現高效的單元測試開發,我們還需要在被測試系統的設計階段,就要把可測試性考慮進來。那什麼是“可測試性設計”?很多有經驗的前輩都能給出一些答案:

  • 對程序員而言,對給定的代碼段,在單元測試中設置方案應該是輕而易舉的,且代碼實現也應該能容易而快速完成。
  • 可測試性不僅是描述軟件是否可測的詞語,而是要實現整個軟件都易於測試。

書中闡述了許多可測試性設計的原則及方法:

  1. 首先是模塊化設計,這個原則一般大家都比較熟悉,其本質就是使系統由獨立模塊組成,每個模塊在設計中都有特定用途,且互相獨立,依賴關係少能有效簡化測試的複雜性。
  2. SOLID設計原則,設計模式中強調的原則,無需贅述。
  3. 上下文中的模塊化設計,即不僅確保系統設計爲由模塊化部件組成,還需進一步意識到該系統(無論看起來有多大多美妙)都應該始終被設計爲另一個更大系統的一部分。
  4. 面向模塊化設計的測試驅動,即使提倡用TDD方式進行開發。

通常編寫測試的程序員會遇到問題如:

  • 不能實例化對象
  • 不能調用方法
  • 不能獲取到結果
  • 無法替代合作者
  • 無法覆蓋方法

歸納起來就是這兩類:“受限訪問”和“無法替代實現的某些部分”。針對以上,書中給出了“可測試設計”的準則:

  1. 避免使用複雜的私有方法。
  2. 避免使用帶final關鍵字的方法。
  3. 避免使用靜態方法(針對你想要使用Test Stub方式的那些方法)。
  4. 謹慎使用 new關鍵字,在方法中實例化對象時,應問自己:要在測試中交換掉該對象碼?如果它是起到協作的作用,且你可能希望逐個測試地修改其實現,則應以某種方式將其傳遞給方法,而不是從該方法內部實例化它。
  5. 避免在構造方法中實現邏輯,如果構造函數包括“需被測試的內容”,應該將這些內容搬離構造函數,以便通過覆蓋方式進行測試。
  6. 避免單例。因單例會使得測試用例編寫比較困難,且單例容易造成一些隱形的錯誤。
  7. 偏向於使用“組合”而非“繼承”編寫類代碼。使用繼承應以用於多態爲目的,如以重用爲目的,則應該使用“組合”方式進行(即在對象中引用來實現原對象的功能,而不是通過繼承來實現)。
  8. 包裝外部庫,即使用第三方庫(或自己的其他jar包方法)時,應再包一層以便替換和測試。
  9. 避免服務查找,採用構造函數傳入方式代替構造函數內部自動查找“服務方法實例”。因這種方式難於使用前面提到的單元測試工具方法來進行用例的編寫。對此可能有同學會提出疑問,如採用Spring框架是不是會有違反這一準則的嫌疑?實際不算,Spring框架的注入仍是遵循由框架本身從對象外部自動進行注入,而不是讓對象本身進行構建。

小結

在看完這本對單元測試系統闡述的書後,最大的感悟就是,對之前很多關於單元測試含糊不清的概念與方法都有了明確的答案,並且還有一套可以遵循的準則。在對照我們系統後,發現有不少是使用到的地方,但也有許多需要改進的地方。至於如何使用書中準則進行實操,將會放在該系列下一篇文章中。

作者:侯嘉遜

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