.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象
該系列第1篇: 講述了如何創(chuàng)造"縫".? "縫"(seam)是需要知道的概念.
本文是第2篇, 介紹的是如何避免在構建對象時寫出不易測試的代碼.?本文的概念性內容大部分都來自Misko Hevery的這篇博客文章.
構建
還是用上文里汽車的例子.
通常情況下, 我們是先去建造汽車, 組裝好汽車后, 我們再去駕駛它.
軟件開發(fā)也類似, 我們應該把對象構造完畢之后, 再去用它. 但是有時候, 開發(fā)者會在構造過程中添加一些程序邏輯. 這就相當于車還沒造完, 我們就駕駛它去兜風了. 這樣做是不太好的.
構造函數是類用來創(chuàng)建其實例對象的方法, 這里的代碼是用來準備該對象的. 但有時開發(fā)者會在構造函數里做一些其它的工作, 例如構建依賴項, 執(zhí)行初始化邏輯等等.
在構造函數(或者更大一點, 指構建的過程)里, 做這些額外的工作會讓測試變得異常困難. 這是因為像初始化依賴項, 調用服務, 設置狀態(tài)的邏輯等這些工作會把用于測試的"縫"弄丟. 導致無法進行mock.
總之在構造的過程中做太多的工作會妨礙測試.
?
危險信號
在構造函數/字段聲明里出現new關鍵字
如果構造函數里需要創(chuàng)建依賴, 那么這就會為該類與依賴項之間創(chuàng)造了緊耦合. 這個之前提過, 所以需要注入依賴. 但是簡單的值類型, 例如字符串, List, Dictionary等還是可以的.
在構造函數/字段聲明里調用靜態(tài)方法
靜態(tài)方法不可以被mock, 也不能被注入.
構造函數出現流程控制邏輯代碼
這樣就很難對邏輯直接進行測試了. 我們只能分別使用不同的方式構造該對象, 測試并確認對象的狀態(tài). 而這個狀態(tài)通常對直接測試是隱藏的. 實際上只要不是賦值代碼, 就有可能是問題代碼.
構造函數里出現非賦值代碼
存在另外一個初始化函數 (也就是說構造函數走了完, 但是對象并沒有被完全初始化)
?
如何解決問題?
不要在構造函數里創(chuàng)建依賴項, 應該注入它們. 然后在構造函數里把它們賦值給類的私有變量.
當需要構建對象圖(一組有引用關系的對象), 也包括對象需要一些構建的參數等情況, 應該使用工廠, 建造者模式, 或者IoC容器的依賴注入等, 目的是把這些對象的構建工作分離出去.
避免在構造函數里寫邏輯代碼, 例如條件, 循環(huán), 計算等等. 也不能把邏輯代碼放在別的方法, 然后調用該方法...
總之就是要避免對象的構建和對象的行為混合到一起,?因為它們在一起就會很難進行測試.
?
最后還有一點, 首先你需要知道, 根據angular的創(chuàng)始人Misko Hevery所說:
對象的構造分兩類, 一種是可注入的, 一種是可new的.
可注入的對象可以由其它的一堆可注入對象組成. 它們可以為 可new的 對象工作. 可注入的對象通常是實現了接口的service, 像什么IUnitOfWork, IRepository, IxxxService等等.
可new的對象就是對象圖里的終點, 例如實體或者值對象(Value Object)等.
為了易于測試, 針對這兩類構造, 有下列規(guī)則:
可注入的對象可以在構造函數請求(注入)其它的可以注入對象, 但是不能在構造函數請求可new的對象.
反過來, 可new的對象可以在構造函數請求其它的可new對象, 但是不能在構造函數請求可注入的對象.
?
例子
第一個例子
這是不對的, 構建的過程中直接new的話, 就會造成緊耦合, 也無法在測試中使用Test Double來代替它們了. 如果測試中不代替它們的話, 有些服務的開銷可能會很大.
?
正確的寫法是使用依賴注入:
第二個例子
該例中, UserController只需要UserService和LoggingService兩個依賴項. 但是UserService又依賴于UserRepository.?
但是這樣寫就不對了, 這會造成UserController和UserRepository間的緊耦合, 而且配置UserService也并不是UserController的責任.
?
正確的寫法是:
而UserService也最好是注入依賴.
?
而如果UserService并不是在構造函數注入UserRepository的話:
那么Controller里就應該這樣寫:
不過最好還是使用構造函數注入的寫法.
?
第三個例子
仔細的說, 該例有不止一處錯誤.
首先它有條件判斷邏輯代碼; 此外它還使用了ApplicationState.IsRunning這個靜態(tài)變量(就是全局狀態(tài)); 而且在構造函數里還做了UserService的配置工作, 這不是UserController的責任.
盡量要避免全局變量, 它無法進行隔離, 測試會遇到麻煩, 例如并行測試時其中一個測試改變了靜態(tài)變量的值就可能導致另一個測試失敗.
但是粗略的說, 該例可以說就是一個錯誤, 如何配置UserService并不是UserController的責任, 所以, 正確的做法是把UserService配置相關的代碼移出去, 讓它自己去管理吧:
?
第四個例子
該例子中, LoggingService的Log方法需要一個Area類型的對象, 它是一個值對象.
所以它的錯誤就是, 不應該把可new的對象注入到可注入的對象里. 這么做的話, 測試就不好做隔離了.
?
正確的做法應該是, 作為方法的參數傳遞進來:
第五個例子
如果出現類類似initalize()或類似意思的方法, 很有可能說明該對象的責任太多了.
?
修改它很簡單, 讓各自的類負責自己的內容即可. 去掉initialize()方法即可.
?
例子就舉這些, 并不全, 詳細請看Angular作者的博文.
?
測試/運行時如何建立對象
上面例子里的UserController就是我們需要使用的對象, 在運行時, 代碼可能是這樣的:
構建這個對象還是有點麻煩的, 它的類關系圖如下:
?
所以測試的設置過程也會比較麻煩:
當然也可以不直接new, 而是使用mock. 總之都很麻煩.
?
使用工廠
所以我們可以使用Factory等模式, 把構建UserController的工作放到工廠里:
?
可以這樣調用:
?
使用IoC容器
如果項目使用了IoC容器的話, 還可以使用類似下面的用法:
?
先介紹到這里.
原文地址:http://www.cnblogs.com/cgzl/p/9375655.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 业务流程、长周期服务和微服务
- 下一篇: 构建可扩展的有状态服务