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

来源:https://www.cnblogs.com/cgzl/archive/2018/07/28/9375655.html
-Advertisement-
Play Games

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


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

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

構建

還是用上文里汽車的例子.

通常情況下, 我們是先去建造汽車, 組裝好汽車後, 我們再去駕駛它.

軟體開發也類似, 我們應該把對象構造完畢之後, 再去用它. 但是有時候, 開發者會在構造過程中添加一些程式邏輯. 這就相當於車還沒造完, 我們就駕駛它去兜風了. 這樣做是不太好的.

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

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

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

 

危險信號

  • 在構造函數/欄位聲明裡出現new關鍵字
    • 如果構造函數里需要創建依賴, 那麼這就會為該類與依賴項之間創造了緊耦合. 這個之前提過, 所以需要註入依賴. 但是簡單的值類型, 例如字元串, List, Dictionary等還是可以的.
  • 在構造函數/欄位聲明裡調用靜態方法
    • 靜態方法不可以被mock, 也不能被註入.
  • 構造函數出現流程式控制制邏輯代碼
    • 這樣就很難對邏輯直接進行測試了. 我們只能分別使用不同的方式構造該對象, 測試並確認對象的狀態. 而這個狀態通常對直接測試是隱藏的. 實際上只要不是賦值代碼, 就有可能是問題代碼.
  • 構造函數里出現非賦值代碼
  • 存在另外一個初始化函數 (也就是說構造函數走了完, 但是對象並沒有被完全初始化)

 

如何解決問題?

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

總之就是要避免對象的構建和對象的行為混合到一起, 因為它們在一起就會很難進行測試.

 

最後還有一點, 首先你需要知道, 根據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容器的話, 還可以使用類似下麵的用法:

 

先介紹到這裡.

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • try:將有可能導致出現異常的語句放到try塊中,如果使用了try語句後,後面的程式必須至少要跟一個except或者finally,否則程式會報錯 except:捕獲try塊中可能出現的異常 finally:不管程式是否有無異常,都會最終執行該語句 for example as below: 結果如 ...
  • C++自定義String字元串類 實現了各種基本操作,包括重載+號實現String的拼接 findSubStr函數,也就是尋找目標串在String中的位置,用到了KMP字元串搜索演算法。 include include using namespace std; class String; class ...
  • Java編程中獲取鍵盤輸入實現方法及註意事項 1. 鍵盤輸入一個數組 package com.wen201807.sort; import java.util.Scanner; public class Main { public static void main(String[] args) { ...
  • 1>:首先在CMD命令行中輸入:fsutil resource setautoreset true c:\ 2>:然後在運行services.msc 3>:找到Windows Process Activation Service服務 啟動該服務,啟動類型:自動 4>:繼續找到World Wide W ...
  • 最近面試時很多面試官都問到了EF框架 好記性不如爛筆頭 趕緊記下來 code-first是EF框架中的一種,是使用實體類來進行資料庫表的映射,所以實體類中的欄位要規範(我認為) 比如: 如果有外鍵的話 一定要搞清楚一對多、多對一和多對多的關係 比如一個用戶對應一個用戶詳細信息可以寫成這樣: 用戶詳細 ...
  • 首先先介紹一下這個項目,該項目實現了文本寫入及讀取,日誌寫入指定文件夾或預設文件夾,日誌數量控制,單個日誌大小控制,通過約定的參數讓用戶可以用更少的代碼解決問題。 1.讀取文本文件方法 使用:JIYUWU.TXT.TXTHelper.ReadToString(“文件物理路徑”) 1 public s ...
  • 一個典型的ASP.NET Core應用程式會包含Program與Startup兩個文件。Program類中有應用程式的入口方法Main,其中的處理邏輯通常是創建一個WebHostBuilder,再生成WebHost,最後啟動之。 而在創建WebHostBuilder時又會常常會指定一個Startup ...
  • 1.資料庫截取字元串:toFixed():四捨五入substring(cp_introduce,0,11) cp_introduce前臺截取: field: 'an_content', title: '問題內容', formatter: function (value) { if (value.le ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...