只有累積,沒有奇蹟

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:

  • [.NET] ASP.NET Application 概述 ASP.NET 處理請求的兩個步驟 當用戶發送一個請求到IIS(這裡指IIS 5.0 & IIS 6.0),ASP.NET 處理請求的步驟有兩種 1. 建立一個可以處理用戶端傳來請求Request的環境(ASP.NET Environment),包括建立application object(應用程式)、Ruquest、Response、Context Object等物件來處理此Ruquest請求。 2. 環境被建立後,應用程式會透過m… Read More
  • [.NET] ASP.NET 特殊資料夾 [Special Folders] 在Visual Studio 2008 建立新的Web網站時,會同時新增一個特別的資料夾叫做「App_Data」,此資料夾是預留給資料庫使用,如Sql Server 2005 Express版,副檔名為「.mdf」的檔案。ASP.NET中有一些特殊的資料夾名稱,可以在專案點右鍵 > 加入 > 加入ASP.NET資料夾;將特殊資料夾加到專案中 ASP.NET Special Folders    … Read More
  • [.NET] ASP.NET 狀態管理(State Management):Session Session 狀態支援數種不同的資料儲存選項,簡單描述 Session 可用的狀態模式 InProc:存在Web伺服器的記憶體中;ASP.NET中預設的Session狀態設定,是最常用也是最方便的 Session 狀態模式,缺點是如果重新啟動伺服器,所有的Session資料將會遺失。 StateServer:儲存在 ASP.NET 狀態服務的處理序中;可以確保 Session 在 Web 應用程式重新啟動時保留下來,並且讓 Web 伺服陣… Read More
  • [.NET] 如何取得 Enum 的 Description 描述字串前言  列舉類型 Enum 在 C# 很常用的一種類型,所允許的型別必須是byte、sbyte、short、ushort、int、uint、long、ulong,在使用上沒特別指定的話基本類型是 int,對我自己來說在程式中使用 Enum 而不用 int 的好處是 Code 閱讀上比較清晰,舉例來說在閱讀代碼時第一段代碼使用 Enum 更容易讓人好懂些 if (code == ResponseCode.OK) //… Read More
  • [C#] Anonymous Type 匿名型別說明 Anonymous Type 是甚麼? 匿名型別是C# 3.0開始有的特性,是一種暫存型的型別,不需要建立額外的類別來存放資料 根據MSDN對於 匿名型別 的說明如下 根據MSDN的說明,整理一下重點及特性 1. 透過 new 建立實體  // Anonymous Typevar employee = new { Id = 1, Name = "Marcus", Age = 22 } … 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