.NET Core TDD 前傳: 編寫易於測試的代碼 -- 縫

有時候不是咱們不想作單元測試, 而是這代碼寫的實在是無法測試....數據庫

舉個例子, 若是一輛汽車在產出後沒完成測試, 那麼沒人敢去駕駛它. 代碼也是同樣的, 若是項目未能進行該作的測試, 那麼客戶就不敢去使用它, 即便使用了也會遇到「車禍」. 編程

 

爲何要測試/測試的好處

  • 它能夠儘早發現bug, 解決bug
  • 它會節省開發和維護一個軟件的總成本. 實際上咱們在維護軟件上付出的成本要遠大於在開發時付出的成本. 開發的時候編寫單元測試確實會增長一些成本, 可是從長遠來看這些測試仍是會從維護上下降軟件的總成本.
  • 它會促使開發者改進設計. 若是開發時先寫測試或者同時寫測試代碼, 那麼開發者會不得不仔細考慮要解決的問題, 因此會寫出更好的設計, 並且無需考慮如何測試代碼.
  • 至關於自成文檔. 由於全部的測試就是被開發軟件全部期待的行爲.
  • 加強自信, 去除恐懼. 有時修改代碼後咱們就會擔憂這是否對現有的功能形成了破壞, 而若是單元測試覆蓋了軟件的重要功能的話, 那麼只要測試都能經過, 那麼就基本能夠確信功能沒被破壞.

測試從不一樣的角度看能夠分紅不少類. 咱們首先應該保證好單元測試可以很好的進行, 只要單元測試可以很好的進行, 那麼其它測試應該均可以很好的進行. app

 

爲何要寫易於測試的代碼

再詳細說一下:框架

在談到軟件測試的時候, 網上的文章常常舉這個建造汽車的例子, 那我也拿汽車這個例子說明問題吧.函數

假設咱們須要設計並生產一輛汽車, 可能會有兩種方式:單元測試

第一種是把車設計成一個複雜的總體, 把全部須要的零件都焊到了一塊兒, 也能夠說它只有一個大零件, 就是汽車自己. 這樣作的好處就是咱們沒必要花那麼多時間和精力去製做發動機, 輪胎, 車窗等等這些可替換的零件了. 這麼去作是有可能把汽車的設計和生產成本下降的. 可是若是汽車被長期使用, 考慮到售後及維護, 那麼成本確定會很是高了.測試

若是汽車壞了, 咱們沒法檢測是哪裏出錯, 由於它是一個總體, 沒法對某部分進行隔離測試; 即便咱們知道哪裏有問題, 咱們仍是沒法替換損壞的部分, 由於它仍是一個總體...spa

 

第二種方式就是正確的方式, 咱們使用可替換的零件進行設計生產, 這樣就會方便測試和售後維護. 由於車裏的每一個零件均可以被替換, 也能夠取出來單獨進行測試. 若是汽車不能啓動, 那麼就對每一個零件進行檢查, 最後替換出問題的零件便可, 而無需像第一種方式那樣把整個車扒開進行大修.設計

很明顯, 正常的汽車廠商都是使用的第二種方式, 由於其具備可測試性可維護性3d

 

軟件開發這個領域和設計汽車是很類似的, 能夠像第一種方式同樣開發軟件, 也能夠像第二種方式同樣開發軟件.

在現實中, 有太多的開發者使用了第一種方式, 把一大堆代碼和功能都放到了一塊兒. 而實際上開發者們應該採用第二種方式來進行代碼的設計和編寫, 即便在開發初期這可能會花掉更多的時間和精力. 

有的時候不是開發者不想採起第二種方式, 而是花了很大力氣卻發現寫出來的代碼仍然不能很好的進行單元測試, 因此實際問題是不知道該如何寫出易於測試的代碼.

 

什麼樣的代碼易於測試

仍是汽車的例子, 若是咱們懷疑汽車的電瓶壞了, 那麼採用第一種方式創造的汽車就沒法進行對它的「電瓶」進行單獨檢測, 由於是焊到一塊兒的, 也沒有能夠用檢測的插頭等; 而採用第二種方式建造的汽車則能夠把電瓶拿出來, 而後咱們使用電壓表等專用的儀器在隔離的狀況下對其進行檢測.

第二種方式之因此能夠進行隔離測試是由於它採用的是可替換零件, 也就是零件能夠拿下來.

用專業的術語說就是第二種方式裏有縫(seam). 在軟件裏, 什麼是縫(seam)? 縫就是你能夠在程序裏替換行爲的地方, 而不須要在這個地方進行修改. 或者說就是可讓你的代碼移除依賴項並建立出可用於隔離測試對象的地方.....我可能解釋的不明白, 看圖吧:

虛線就是縫.

 

因爲有縫的存在, 因此咱們能夠進行隔離測試:

分別使用Test FixtureTest double來替換調用類和依賴項.

而採用第一種方式的軟件就沒法把代碼拆出來進行測試了, 由於沒法替換依賴項, 沒法接入到測試環境, 也就是說沒法進行隔離測試了.

 

爲何代碼會沒法進行隔離測試呢

沒法測試的代碼有一些特色:

  • new 關鍵字. 若是這部分代碼裏出現了new關鍵字, 也就是說在構造函數或方法內創造了外部資源或較複雜類型的實例, 那麼測試就會很困難了. 而應該採用的作法是依賴注入.
  • 靜態方法/屬性調用. 靜態方法會爲它的調用者和它被調用時所在的類建立很緊的耦合. 使用像Math.Min(), String.Join()這些方法時是沒有題的, 可是若是使用DateTime.Now, Console.Write() 那就可能會出問題了. 這時候你可能就須要使用一個包裝類了.
  • 單立體 Singleton. Singleton的本質是共享狀態. 可是爲了隔離測試, 最好仍是避免使用singleton. 若是確實須要使用它的話, 那麼在測試的時候可使用一個非Singleton的替身來進行測試, 固然, 經過依賴注入.
  • 全局共享狀態, 這個應該明白
  • 引用第三方框架或外部資源. 一旦有這樣的引用的話, 就沒法進行隔離測試了. 咱們須要作的就是對這些東西抽象化, 把細節忽略只關心特定條件下的特定結果.

 

如何產生縫隙

  • 解藕依賴項. 在C#裏, 咱們經過對接口編程而不是對實現來編程來實現這個任務. 
  • 依賴注入. 主要是採用構造函數注入.

作到這兩點, 那麼咱們就可使用test double(測試替身)來代替依賴項並注入到被測試類使用, 從而進行隔離測試.

 

例子

下面就是一個難以測試的例子, 這個代碼並不完美, 沒法展現出不可測試代碼全部的特色, 可是也包含了至少兩個特色:

首先它的依賴項都是new出來的, 這些依賴項就有依賴於數據庫的, 因此測試的話, 咱們還須要知道數據庫裏面特定的數據內容..這樣的結果就是測試很難完成.

其次這裏用到了第三方的Mapper.Map()靜態方法, 這個方法也許是通過測試的而且沒有反作用的, 可是也有可能不是. 並且它形成了ProductControllerHard和Mapper類之間的緊耦合.

 

針對第一個問題, 我想都知道怎麼去處理了, 就是使用接口. 我就很少介紹了.

針對第二個問題, 使用靜態方法形成了緊耦合. 若是這個靜態方法是咱們本身寫的方法, 咱們能夠對其重構, 變成實例方法. 可是若是它來自第三方庫, 而且第三方庫沒有提供能夠依賴注入使用的版本, 那麼咱們本身能夠寫一個包裝類(wrapper)來包裝該方法:

可是因爲這個Mapper來自AutoMapper庫, 這個庫提供了IMapper接口, 因此使用IMapper進行依賴注入便可.

 

可測試的代碼應該以下:

相關文章
相關標籤/搜索