.NET Core TDD前传: 编写易于测试的代码 -- 缝
有時(shí)候不是我們不想做單元測(cè)試, 而是這代碼寫的實(shí)在是沒(méi)法測(cè)試....
舉個(gè)例子, 如果一輛汽車在產(chǎn)出后沒(méi)完成測(cè)試, 那么沒(méi)人敢去駕駛它. 代碼也是一樣的, 如果項(xiàng)目未能進(jìn)行該做的測(cè)試, 那么客戶就不敢去使用它, 即使使用了也會(huì)遇到“車禍”.?
?
為什么要測(cè)試/測(cè)試的好處
它可以盡早發(fā)現(xiàn)bug, 解決bug
它會(huì)節(jié)省開(kāi)發(fā)和維護(hù)一個(gè)軟件的總成本. 實(shí)際上我們?cè)诰S護(hù)軟件上付出的成本要遠(yuǎn)大于在開(kāi)發(fā)時(shí)付出的成本. 開(kāi)發(fā)的時(shí)候編寫單元測(cè)試確實(shí)會(huì)增加一些成本, 但是從長(zhǎng)遠(yuǎn)來(lái)看這些測(cè)試還是會(huì)從維護(hù)上降低軟件的總成本.
它會(huì)促使開(kāi)發(fā)者改進(jìn)設(shè)計(jì). 如果開(kāi)發(fā)時(shí)先寫測(cè)試或者同時(shí)寫測(cè)試代碼, 那么開(kāi)發(fā)者會(huì)不得不仔細(xì)考慮要解決的問(wèn)題, 所以會(huì)寫出更好的設(shè)計(jì), 而且無(wú)需考慮如何測(cè)試代碼.
相當(dāng)于自成文檔. 因?yàn)樗械臏y(cè)試就是被開(kāi)發(fā)軟件所有期待的行為.
增強(qiáng)自信, 去除恐懼. 有時(shí)修改代碼后我們就會(huì)擔(dān)心這是否對(duì)現(xiàn)有的功能造成了破壞, 而如果單元測(cè)試覆蓋了軟件的重要功能的話, 那么只要測(cè)試都能通過(guò), 那么就基本可以確信功能沒(méi)被破壞.
測(cè)試從不同的角度看可以分成很多類. 我們首先應(yīng)該保證好單元測(cè)試能夠很好的進(jìn)行, 只要單元測(cè)試能夠很好的進(jìn)行, 那么其它測(cè)試應(yīng)該都可以很好的進(jìn)行.?
?
為什么要寫易于測(cè)試的代碼
再詳細(xì)說(shuō)一下:
在談到軟件測(cè)試的時(shí)候, 網(wǎng)上的文章經(jīng)常舉這個(gè)建造汽車的例子, 那我也拿汽車這個(gè)例子說(shuō)明問(wèn)題吧.
假設(shè)我們需要設(shè)計(jì)并生產(chǎn)一輛汽車, 可能會(huì)有兩種方式:
第一種是把車設(shè)計(jì)成一個(gè)復(fù)雜的整體, 把所有需要的零件都焊到了一起, 也可以說(shuō)它只有一個(gè)大零件, 就是汽車本身. 這樣做的好處就是我們不必花那么多時(shí)間和精力去制作發(fā)動(dòng)機(jī), 輪胎, 車窗等等這些可替換的零件了. 這么去做是有可能把汽車的設(shè)計(jì)和生產(chǎn)成本降低的. 但是如果汽車被長(zhǎng)期使用, 考慮到售后及維護(hù), 那么成本肯定會(huì)非常高了.
如果汽車壞了, 我們無(wú)法檢測(cè)是哪里出錯(cuò), 因?yàn)樗且粋€(gè)整體, 無(wú)法對(duì)某部分進(jìn)行隔離測(cè)試; 即使我們知道哪里有問(wèn)題, 我們還是無(wú)法替換損壞的部分, 因?yàn)樗€是一個(gè)整體...
?
第二種方式就是正確的方式, 我們使用可替換的零件進(jìn)行設(shè)計(jì)生產(chǎn), 這樣就會(huì)方便測(cè)試和售后維護(hù). 因?yàn)檐嚴(yán)锏拿總€(gè)零件都可以被替換, 也可以取出來(lái)單獨(dú)進(jìn)行測(cè)試. 如果汽車不能啟動(dòng), 那么就對(duì)每個(gè)零件進(jìn)行檢查, 最后替換出問(wèn)題的零件即可, 而無(wú)需像第一種方式那樣把整個(gè)車扒開(kāi)進(jìn)行大修.
很明顯, 正常的汽車廠商都是使用的第二種方式, 因?yàn)槠渚哂?strong>可測(cè)試性和可維護(hù)性.?
?
軟件開(kāi)發(fā)這個(gè)領(lǐng)域和設(shè)計(jì)汽車是很相似的, 可以像第一種方式一樣開(kāi)發(fā)軟件, 也可以像第二種方式一樣開(kāi)發(fā)軟件.
在現(xiàn)實(shí)中, 有太多的開(kāi)發(fā)者使用了第一種方式, 把一大堆代碼和功能都放到了一起. 而實(shí)際上開(kāi)發(fā)者們應(yīng)該采用第二種方式來(lái)進(jìn)行代碼的設(shè)計(jì)和編寫, 即使在開(kāi)發(fā)初期這可能會(huì)花掉更多的時(shí)間和精力.?
有的時(shí)候不是開(kāi)發(fā)者不想采取第二種方式, 而是花了很大力氣卻發(fā)現(xiàn)寫出來(lái)的代碼仍然不能很好的進(jìn)行單元測(cè)試, 所以實(shí)際問(wèn)題是不知道該如何寫出易于測(cè)試的代碼.
?
什么樣的代碼易于測(cè)試
還是汽車的例子, 如果我們懷疑汽車的電瓶壞了, 那么采用第一種方式創(chuàng)造的汽車就無(wú)法進(jìn)行對(duì)它的“電瓶”進(jìn)行單獨(dú)檢測(cè), 因?yàn)槭呛傅揭黄鸬? 也沒(méi)有可以用檢測(cè)的插頭等; 而采用第二種方式建造的汽車則可以把電瓶拿出來(lái), 然后我們使用電壓表等專用的儀器在隔離的情況下對(duì)其進(jìn)行檢測(cè).
第二種方式之所以可以進(jìn)行隔離測(cè)試是因?yàn)樗捎玫氖强商鎿Q零件, 也就是零件可以拿下來(lái).
用專業(yè)的術(shù)語(yǔ)說(shuō)就是第二種方式里有縫(seam). 在軟件里, 什么是縫(seam)? 縫就是你可以在程序里替換行為的地方, 而不需要在這個(gè)地方進(jìn)行修改. 或者說(shuō)就是可以讓你的代碼移除依賴項(xiàng)并創(chuàng)建出可用于隔離測(cè)試對(duì)象的地方.....我可能解釋的不明白, 看圖吧:
虛線就是縫.
?
由于有縫的存在, 所以我們可以進(jìn)行隔離測(cè)試:
分別使用Test Fixture和Test double來(lái)替換調(diào)用類和依賴項(xiàng).
而采用第一種方式的軟件就無(wú)法把代碼拆出來(lái)進(jìn)行測(cè)試了, 因?yàn)闊o(wú)法替換依賴項(xiàng), 無(wú)法接入到測(cè)試環(huán)境, 也就是說(shuō)無(wú)法進(jìn)行隔離測(cè)試了.
?
為什么代碼會(huì)無(wú)法進(jìn)行隔離測(cè)試呢
無(wú)法測(cè)試的代碼有一些特點(diǎn):
new 關(guān)鍵字. 如果這部分代碼里出現(xiàn)了new關(guān)鍵字, 也就是說(shuō)在構(gòu)造函數(shù)或方法內(nèi)創(chuàng)造了外部資源或較復(fù)雜類型的實(shí)例, 那么測(cè)試就會(huì)很困難了. 而應(yīng)該采用的做法是依賴注入.
靜態(tài)方法/屬性調(diào)用. 靜態(tài)方法會(huì)為它的調(diào)用者和它被調(diào)用時(shí)所在的類創(chuàng)建很緊的耦合. 使用像Math.Min(), String.Join()這些方法時(shí)是沒(méi)有題的, 但是如果使用DateTime.Now, Console.Write() 那就可能會(huì)出問(wèn)題了. 這時(shí)候你可能就需要使用一個(gè)包裝類了.
單立體 Singleton. Singleton的本質(zhì)是共享狀態(tài). 但是為了隔離測(cè)試, 最好還是避免使用singleton. 如果確實(shí)需要使用它的話, 那么在測(cè)試的時(shí)候可以使用一個(gè)非Singleton的替身來(lái)進(jìn)行測(cè)試, 當(dāng)然, 通過(guò)依賴注入.
全局共享狀態(tài), 這個(gè)應(yīng)該明白
引用第三方框架或外部資源. 一旦有這樣的引用的話, 就無(wú)法進(jìn)行隔離測(cè)試了. 我們需要做的就是對(duì)這些東西抽象化, 把細(xì)節(jié)忽略只關(guān)心特定條件下的特定結(jié)果.
?
如何產(chǎn)生縫隙
解藕依賴項(xiàng). 在C#里, 我們通過(guò)對(duì)接口編程而不是對(duì)實(shí)現(xiàn)來(lái)編程來(lái)實(shí)現(xiàn)這個(gè)任務(wù).?
依賴注入. 主要是采用構(gòu)造函數(shù)注入.
做到這兩點(diǎn), 那么我們就可以使用test double(測(cè)試替身)來(lái)代替依賴項(xiàng)并注入到被測(cè)試類使用, 從而進(jìn)行隔離測(cè)試.
?
例子
下面就是一個(gè)難以測(cè)試的例子, 這個(gè)代碼并不完美, 無(wú)法展示出不可測(cè)試代碼所有的特點(diǎn), 但是也包含了至少兩個(gè)特點(diǎn):
首先它的依賴項(xiàng)都是new出來(lái)的, 這些依賴項(xiàng)就有依賴于數(shù)據(jù)庫(kù)的, 所以測(cè)試的話, 我們還需要知道數(shù)據(jù)庫(kù)里面特定的數(shù)據(jù)內(nèi)容..這樣的結(jié)果就是測(cè)試很難完成.
其次這里用到了第三方的Mapper.Map()靜態(tài)方法, 這個(gè)方法也許是經(jīng)過(guò)測(cè)試的并且沒(méi)有副作用的, 但是也有可能不是. 而且它造成了ProductControllerHard和Mapper類之間的緊耦合.
?
針對(duì)第一個(gè)問(wèn)題, 我想都知道怎么去處理了, 就是使用接口. 我就不多介紹了.
針對(duì)第二個(gè)問(wèn)題, 使用靜態(tài)方法造成了緊耦合. 如果這個(gè)靜態(tài)方法是我們自己寫的方法, 我們可以對(duì)其重構(gòu), 變成實(shí)例方法. 但是如果它來(lái)自第三方庫(kù), 并且第三方庫(kù)沒(méi)有提供可以依賴注入使用的版本, 那么我們自己可以寫一個(gè)包裝類(wrapper)來(lái)包裝該方法:
但是由于這個(gè)Mapper來(lái)自AutoMapper庫(kù), 這個(gè)庫(kù)提供了IMapper接口, 所以使用IMapper進(jìn)行依賴注入即可.
?
可測(cè)試的代碼應(yīng)該如下:
原文地址:https://www.cnblogs.com/cgzl/p/9365955.html
.NET社區(qū)新聞,深度好文,歡迎訪問(wèn)公眾號(hào)文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的.NET Core TDD前传: 编写易于测试的代码 -- 缝的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 微软发布Azure Service Fa
- 下一篇: 【招聘(北京)】东方国信 工业互联网