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