.NET Core TDD 前傳: 編寫易於測試的代碼 -- 構建對象

該系列第1篇: 講述了如何創造"縫".  "縫"(seam)是須要知道的概念.html

本文是第2篇, 介紹的是如何避免在構建對象時寫出不易測試的代碼. 本文的概念性內容大部分都來自Misko Hevery的這篇博客文章.ide

構建

仍是用上文裏汽車的例子.函數

一般狀況下, 咱們是先去建造汽車, 組裝好汽車後, 咱們再去駕駛它.測試

軟件開發也相似, 咱們應該把對象構造完畢以後, 再去用它. 可是有時候, 開發者會在構造過程當中添加一些程序邏輯. 這就至關於車還沒造完, 咱們就駕駛它去兜風了. 這樣作是不太好的.ui

構造函數是類用來建立其實例對象的方法, 這裏的代碼是用來準備該對象的. 但有時開發者會在構造函數裏作一些其它的工做, 例如構建依賴項, 執行初始化邏輯等等.spa

在構造函數(或者更大一點, 指構建的過程)裏, 作這些額外的工做會讓測試變得異常困難. 這是由於像初始化依賴項, 調用服務, 設置狀態的邏輯等這些工做會把用於測試的"縫"弄丟. 致使沒法進行mock.3d

總之在構造的過程當中作太多的工做會妨礙測試.code

 

危險信號

  • 在構造函數/字段聲明裏出現new關鍵字
    • 若是構造函數裏須要建立依賴, 那麼這就會爲該類與依賴項之間創造了緊耦合. 這個以前提過, 因此須要注入依賴. 可是簡單的值類型, 例如字符串, List, Dictionary等仍是能夠的.
  • 在構造函數/字段聲明裏調用靜態方法
    • 靜態方法不能夠被mock, 也不能被注入.
  • 構造函數出現流程控制邏輯代碼
    • 這樣就很難對邏輯直接進行測試了. 咱們只能分別使用不一樣的方式構造該對象, 測試並確認對象的狀態. 而這個狀態一般對直接測試是隱藏的. 實際上只要不是賦值代碼, 就有多是問題代碼.
  • 構造函數裏出現非賦值代碼
  • 存在另一個初始化函數 (也就是說構造函數走了完, 可是對象並無被徹底初始化)

 

如何解決問題?

  • 不要在構造函數裏建立依賴項, 應該注入它們. 而後在構造函數裏把它們賦值給類的私有變量.
  • 當須要構建對象圖(一組有引用關係的對象), 也包括對象須要一些構建的參數等狀況, 應該使用工廠, 建造者模式, 或者IoC容器的依賴注入等, 目的是把這些對象的構建工做分離出去.
  • 避免在構造函數裏寫邏輯代碼, 例如條件, 循環, 計算等等. 也不能把邏輯代碼放在別的方法, 而後調用該方法...

總之就是要避免對象的構建和對象的行爲混合到一塊兒, 由於它們在一塊兒就會很難進行測試.htm

 

最後還有一點, 首先你須要知道, 根據angular的創始人Misko Hevery所說:對象

對象的構造分兩類, 一種是可注入的, 一種是可new的.

可注入的對象能夠由其它的一堆可注入對象組成. 它們能夠爲 可new的 對象工做. 可注入的對象一般是實現了接口的service, 像什麼IUnitOfWork, IRepository, IxxxService等等.

可new的對象就是對象圖裏的終點, 例如實體或者值對象(Value Object)等.

爲了易於測試, 針對這兩類構造, 有下列規則:

可注入的對象能夠在構造函數請求(注入)其它的能夠注入對象, 可是不能在構造函數請求可new的對象.

反過來, 可new的對象能夠在構造函數請求其它的可new對象, 可是不能在構造函數請求可注入的對象.

 

例子

第一個例子

這是不對的, 構建的過程當中直接new的話, 就會形成緊耦合, 也沒法在測試中使用Test Double來代替它們了. 若是測試中不代替它們的話, 有些服務的開銷可能會很大.

 

正確的寫法是使用依賴注入:

第二個例子

該例中, UserController只須要UserService和LoggingService兩個依賴項. 可是UserService又依賴於UserRepository. 

可是這樣寫就不對了, 這會形成UserController和UserRepository間的緊耦合, 並且配置UserService也並非UserController的責任.

 

正確的寫法是:

而UserService也最好是注入依賴.

 

而若是UserService並非在構造函數注入UserRepository的話:

那麼Controller裏就應該這樣寫:

不過最好仍是使用構造函數注入的寫法.

 

第三個例子

仔細的說, 該例有不止一處錯誤.

首先它有條件判斷邏輯代碼; 此外它還使用了ApplicationState.IsRunning這個靜態變量(就是全局狀態); 並且在構造函數裏還作了UserService的配置工做, 這不是UserController的責任.

儘可能要避免全局變量, 它沒法進行隔離, 測試會遇到麻煩, 例如並行測試時其中一個測試改變了靜態變量的值就可能致使另外一個測試失敗.

可是粗略的說, 該例能夠說就是一個錯誤, 如何配置UserService並非UserController的責任, 因此, 正確的作法是把UserService配置相關的代碼移出去, 讓它本身去管理吧:

 

第四個例子

該例子中, LoggingService的Log方法須要一個Area類型的對象, 它是一個值對象.

因此它的錯誤就是, 不該該把可new的對象注入到可注入的對象裏. 這麼作的話, 測試就很差作隔離了.

 

正確的作法應該是, 做爲方法的參數傳遞進來:

第五個例子

若是出現類相似initalize()或相似意思的方法, 頗有可能說明該對象的責任太多了.

 

修改它很簡單, 讓各自的類負責本身的內容便可. 去掉initialize()方法便可.

 

例子就舉這些, 並不全, 詳細請看Angular做者的博文.

 

測試/運行時如何創建對象

上面例子裏的UserController就是咱們須要使用的對象, 在運行時, 代碼多是這樣的:

構建這個對象仍是有點麻煩的, 它的類關係圖以下:

 

因此測試的設置過程也會比較麻煩:

固然也能夠不直接new, 而是使用mock. 總之都很麻煩.

 

使用工廠

因此咱們可使用Factory等模式, 把構建UserController的工做放到工廠裏:

 

能夠這樣調用:

 

使用IoC容器

若是項目使用了IoC容器的話, 還可使用相似下面的用法:

 

先介紹到這裏.

相關文章
相關標籤/搜索