只有累積,沒有奇蹟

2019年7月25日 星期四

[UnitTest] 如何測試目標方法中含有 static method 代碼 ?

情境
由於部門過去的 專案幾乎都沒有加上單元測試進行保護,主管在新的一年規劃中開發代碼更有品質,希望開發的專案加上新功能或是修改時要加上單元測試,有些 Legacy Code 寫法是屬於一條龍式的 「義大利麵式碼」特別有親切感(大誤,遇到這種就需要先重構 (refactor) 後物件化比較好寫單元測試,今天主題是單元測試中要被測試的 method 中常常會有相依於某個 Static method在寫 Code 時用 static 物件寫起來很方便,但遇到這種情況在單元測試就需要多下點功夫,以下簡單筆記如何測試 static class 的方法

解決方案
舉個例子來說有個需求要去呼叫第三方取得相關資訊,定義一個 ThirdParty 類別其中透過 SendRequest 方法來送出 Request 與第三方 API 溝通,使用 static 的 LoggerHelper 記錄第三方回傳的內容 response 物件其中是 Legacy Code 紀錄 log 方式是 Log message 寫到網路硬碟Sample Code 如下
  1. using System;
  2. using NLog;
  3.  
  4. namespace ConsoleApp1
  5. {
  6. public class Program
  7. {
  8. static void Main(string[] args)
  9. {
  10. var thirdParty = new ThirdPartyClass();
  11. thirdParty.SendRequest();
  12.  
  13. Console.ReadKey();
  14. }
  15. }
  16.  
  17. public class ThirdPartyClass
  18. {
  19. public void SendRequest()
  20. {
  21. //... call 3rd server
  22. var response = _apiService.PostData();
  23.  
  24. // log response
  25. LoggerHelper.Info($"Call 3rd response is : {response}");
  26. //... more
  27. }
  28. }
  29. public static class LoggerHelper
  30. {
  31. private static string filePath = "d:\\logs\\";
  32.  
  33. public static void Info(string message)
  34. {
  35. // 依賴於網路硬碟 X:\logs\info
  36. }
  37.  
  38. public static void Error(string message)
  39. {
  40. // 依賴於網路硬碟 X:\logs\error
  41. }
  42. }
  43.  
  44. [Test()]
  45. public void ThirdPartyClassTest()
  46. {
  47. ThirdPartyClass thirdParty = new ThirdPartyClass();
  48. thirdParty.SendRequest();
  49. }
  50. }
在之前上 TDD 課時候老師有提到 單元測試準則 : 一次只驗證一件事 SendRequest 方法直接耦合static function 無法進行隔離測試,91哥也在 文章 中指出直接相依 static function 的主要問題是
  • 無謂地佔住記憶體過久
  • 直接耦合造成無法獨立進行單元測試Line 15 : 取得全部參數方法 
  • 無法享用物件導向設計的好處(繼承的重用與擴充、介面的可抽換性、多型的擴充性)
  • race condition
需要針對此測試方法進行解耦的設計讓 ThirdPartyClass 與 LoggerHelper 都依賴於某物件互相不直接耦合 以達到解耦合的效果過去有學到很多種方法可以達到這件事情,本次介紹的是透過 interface 來解決這問題,說明如下

Step 1 : 首先先建立一個 ILoggerHelper 介面讓 LoggerHelper 實作  
備註 : Resharper 快捷鍵 : Ctrl + R ,I
LoggerHelper 實作 ILoggerHelper 後 Code 如下
  1. public class LoggerHelper : ILoggerHelper
  2. {
  3. private string filePath = @"d:\\logs\\";
  4.  
  5. public void Info(string message)
  6. {
  7. // 依賴於網路硬碟 X:\logs\info
  8. }
  9.  
  10. public void Error(string message)
  11. {
  12. // 依賴於網路硬碟 X:\logs\error
  13. }
  14. }
  15.  
  16. public interface ILoggerHelper
  17. {
  18. void Error(string message);
  19. void Info(string message);
  20. }
Step 2 : 此時原本的 ThirdPartyClass 原本依賴的 static LoggerHelper 方法無法使用會出現 error,如下圖所示
Step 3 : 讓 SendRequest 耦合於 ILoggerHelper 介面ThirdParty Class code 如下
  1. public class ThirdPartyClass
  2. {
  3. private ILoggerHelper _logger;
  4.  
  5. public ThirdPartyClass(ILoggerHelper logger)
  6. {
  7. _logger = logger;
  8. }
  9. public void SendRequest()
  10. {
  11. //... call 3rd server
  12. //var response = _apiService.PostData();
  13.  
  14. // log response
  15. _logger.Info($"Call 3rd response is :");
  16. //... more
  17. }
  18. }
程式說明 : 
  • 使用建構式注入 _logger  [ 快捷鍵 :  ctorf ]
  • 寫 log 方式由原先 LoggerHelper 改用 _logger
Step 4 : 在測試建立一個 fakeLoggerHelper 實作 ILoggerHelper 介面,並產生相對應實作方法 Implement  missing member,接著 ThirdPartyClassLog 建構子注入 fakeLoggerHelperCode 如下
  1. [TestFixture()]
  2. public class ThirdPartyClassTests
  3. {
  4. [Test()]
  5. public void ThirdPartyClassTest()
  6. {
  7. ILoggerHelper fakeLoggerHelper = new fakeLoggerHelper();
  8. ThirdPartyClass thirdParty = new ThirdPartyClass(fakeLoggerHelper);
  9. thirdParty.SendRequest();
  10. }
  11.  
  12. private class fakeLoggerHelper : ILoggerHelper
  13. {
  14. public void Error(string message)
  15. {
  16. // do something
  17. }
  18.  
  19. public void Info(string message)
  20. {
  21. // do something
  22. }
  23. }
  24. }
Step 4 : 重跑一次測試,綠燈 Pass 測試成功 !!
Summary
這篇文章是讓測試物件依賴於 interface 來解決測試 static method,其他非 static  method 大多使用 extract & overrite 來處理,想了解更多細節可以看 91大大分享的 [Unit Test Tricks] Extract and Override會讓自己對單元測試了解更多,但建議還是要搭配實務才可以驗證到底自己是否真正了解,否則久了沒用(老了?)有一天還是容易忘記

參考

Related Posts:

  • [NETCore] 如何設定 ASP.NET Core 健康檢查(Health Check)功能 - Health Check UI前言 在前一篇針對 ASP.NET Core 2.2 新特性 Health Check功能做基本的介紹,接著要分享的是在 BeatPulse 中實用的功能 Health Check UI,提供 UI 介面顯示及儲存 Health Check 檢查的結果內容,如果有多台時也可以在設定檔加上指定 URL 達到同時監控多台的效果,此篇就針對 Health Check UI 做介紹,若有問題或是錯誤的地方歡迎網路的高手大大給予指導。 Health… Read More
  • [NETCore] 如何設定 ASP.NET Core 健康檢查(Health Check)功能前言 過去當應用程式開發完成之後,另外一項重要的事情就是建立上線後的監控機制,尤其是重要性高的服務更是不可或缺的事情,有遇過公司是在站台底下放置一個檔案內容文字是OK,在透過工具固定時間去確認網站底下這檔案是否有正常回傳,就可代表網站是否存活者,各種不同的實作方式都可以達到此目的;在 ASP.NET Core 2.2 開始提供 Health Check 中介層 (Middleware),透過 HTTP 方式可以即時取得應用程式的健康狀況… Read More
  • [NETCore] 動態 String 字串相加效能比較 前言 在前一篇 [.NETCore] String 字串相加效能比較 對於 C# string 的應用做了一些測試,得到在使用固定字串相加時使用 string 效能反而比 stringBuilder 來的好,在 string  有多種應用情境因此這篇就在針對另一種使用情境針對 string 動態文字相加做比較若有問題或是錯誤的地方歡迎各位給予指導及討論。 測試代碼  測試方式同樣以  B… Read More
  • [NETCore] String 字串相加效能比較 前言 在 .NET 應用程式中很常使用到 string 型別,string 是不可變 ( Immutable ) 的,當每次建立完就會固定其長度,如果要做相加就必須捨棄原有使用的記憶體,在重新配置一塊新的記憶體給它使用,如果在需要大量得字串動態相加時就會影響到其效能,因此在動態文字相加情境就可以透過 stringBuilder 來改善此問題,詳細細節可以參考黑暗大的 StringBuilder串接字串的迷思,這篇重點是在 C# 有提供多… Read More
  • [.NET] Quartz.NET 排程執行異常 - All triggers of Job class set to ERROR state. 問題  最近因為新專案需求是定期到資料庫檢查會員的資料,因此使用 ASP.NET Core Hosting 服務 +  Quartz.NET 作為工作排程器使用,在開發完畢要啟動 Job 驗證正確性時意外跳出異常訊息,錯誤訊息為  Quartz.Simpl.RAMJobStore | All triggers of Job GroupName.ClassName set to ERROR state.&nb… Read More

1 則留言:

  1. 謝謝您的文章, 讓人受益良多! 因為本人在單元測試方面經驗不足, 想請教兩個問題:

    1.在你文章提到的解耦過程中, 將靜態類別改成不是靜態類別, 然後再去實作界面的方法。所以是不是一般在開發上, 盡量不要去寫靜態類別或方法, 以免不利於單元測試, 我這麼想對嗎? 一般都是這麼做的嗎?

    2. 您有建立一個fakeLoggerHelper類別, 並用它的物件來做測試, 請問為什麼不直接用LoggerHelper的物件來做單元測試? 是不是因為它裡面依賴於網路硬碟, 所以拿來做單元測試不適合, 因為會牽扯太多層面而導致太複雜, 所以才創一個 fakeLoggerHelper類別來代替? 那麼這種手法就是所謂的"隔離"嗎? 因為有聽過這個名詞但不太確定。

    謝謝

    回覆刪除

Copyright © 2025 m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com