.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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...