由於部門過去的 專案幾乎都沒有加上單元測試進行保護,主管在新的一年規劃中開發代碼更有品質,希望開發的專案加上新功能或是修改時要加上單元測試,有些 Legacy Code 寫法是屬於一條龍式的 「義大利麵式碼」
解決方案
using System; using NLog; namespace ConsoleApp1 { public class Program { static void Main(string[] args) { var thirdParty = new ThirdPartyClass(); thirdParty.SendRequest(); Console.ReadKey(); } } public class ThirdPartyClass { public void SendRequest() { //... call 3rd server var response = _apiService.PostData(); // log response LoggerHelper.Info($"Call 3rd response is : {response}"); //... more } } public static class LoggerHelper { private static string filePath = "d:\\logs\\"; public static void Info(string message) { // 依賴於網路硬碟 X:\logs\info } public static void Error(string message) { // 依賴於網路硬碟 X:\logs\error } } [Test()] public void ThirdPartyClassTest() { ThirdPartyClass thirdParty = new ThirdPartyClass(); thirdParty.SendRequest(); } }在之前上 TDD 課時候老師有提到 單元測試準則 : 一次只驗證一件事 ,SendRequest 方法直接耦合static function 無法進行隔離測試,91哥也在 文章 中指出直接相依 static function 的主要問題是
- 無謂地佔住記憶體過久
- 直接耦合造成無法獨立進行單元測試Line 15 : 取得全部參數方法
- 無法享用物件導向設計的好處(繼承的重用與擴充、介面的可抽換性、多型的擴充性)
- race condition
Step 1 : 首先先建立一個 ILoggerHelper 介面讓 LoggerHelper 實作
備註 : Resharper 快捷鍵 : Ctrl + R ,I
LoggerHelper 實作 ILoggerHelper 後 Code 如下
public class LoggerHelper : ILoggerHelper { private string filePath = @"d:\\logs\\"; public void Info(string message) { // 依賴於網路硬碟 X:\logs\info } public void Error(string message) { // 依賴於網路硬碟 X:\logs\error } } public interface ILoggerHelper { void Error(string message); void Info(string message); }Step 2 : 此時原本的 ThirdPartyClass 原本依賴的 static LoggerHelper 方法無法使用會出現 error,如下圖所示
Step 3 : 讓 SendRequest 耦合於 ILoggerHelper 介面,ThirdParty Class code 如下
public class ThirdPartyClass { private ILoggerHelper _logger; public ThirdPartyClass(ILoggerHelper logger) { _logger = logger; } public void SendRequest() { //... call 3rd server //var response = _apiService.PostData(); // log response _logger.Info($"Call 3rd response is :"); //... more } }程式說明 :
- 使用建構式注入 _logger [ 快捷鍵 : ctorf ]
- 寫 log 方式由原先 LoggerHelper 改用 _logger
[TestFixture()] public class ThirdPartyClassTests { [Test()] public void ThirdPartyClassTest() { ILoggerHelper fakeLoggerHelper = new fakeLoggerHelper(); ThirdPartyClass thirdParty = new ThirdPartyClass(fakeLoggerHelper); thirdParty.SendRequest(); } private class fakeLoggerHelper : ILoggerHelper { public void Error(string message) { // do something } public void Info(string message) { // do something } } }
Step 4 : 重跑一次測試,綠燈 Pass 測試成功 !!
Summary
這篇文章是讓測試物件依賴於 interface 來解決測試 static method,其他非 static method 大多使用 extract & overrite 來處理,想了解更多細節可以看 91大大分享的 [Unit Test Tricks] Extract and Override會讓自己對單元測試了解更多,但建議還是要搭配實務才可以驗證到底自己是否真正了解,否則久了沒用(老了?)有一天還是容易忘記
參考這篇文章是讓測試物件依賴於 interface 來解決測試 static method,其他非 static method 大多使用 extract & overrite 來處理,想了解更多細節可以看 91大大分享的 [Unit Test Tricks] Extract and Override會讓自己對單元測試了解更多,但建議還是要搭配實務才可以驗證到底自己是否真正了解,否則久了沒用
C# Test Legacy Code(4)Unit Test with Static Functions
How to mock static methods in c# using MOQ framework?
How to mock static methods in c# using MOQ framework?
謝謝您的文章, 讓人受益良多! 因為本人在單元測試方面經驗不足, 想請教兩個問題:
回覆刪除1.在你文章提到的解耦過程中, 將靜態類別改成不是靜態類別, 然後再去實作界面的方法。所以是不是一般在開發上, 盡量不要去寫靜態類別或方法, 以免不利於單元測試, 我這麼想對嗎? 一般都是這麼做的嗎?
2. 您有建立一個fakeLoggerHelper類別, 並用它的物件來做測試, 請問為什麼不直接用LoggerHelper的物件來做單元測試? 是不是因為它裡面依賴於網路硬碟, 所以拿來做單元測試不適合, 因為會牽扯太多層面而導致太複雜, 所以才創一個 fakeLoggerHelper類別來代替? 那麼這種手法就是所謂的"隔離"嗎? 因為有聽過這個名詞但不太確定。
謝謝