由於部門過去的 專案幾乎都沒有加上單元測試進行保護,主管在新的一年規劃中開發代碼更有品質,希望開發的專案加上新功能或是修改時要加上單元測試,有些 Legacy Code 寫法是屬於一條龍式的 「義大利麵式碼」
解決方案
在之前上 TDD 課時候老師有提到 單元測試準則 : 一次只驗證一件事 ,SendRequest 方法直接耦合static function 無法進行隔離測試,91哥也在 文章 中指出直接相依 static function 的主要問題是
- 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();
- }
- }
- 無謂地佔住記憶體過久
- 直接耦合造成無法獨立進行單元測試Line 15 : 取得全部參數方法
- 無法享用物件導向設計的好處(繼承的重用與擴充、介面的可抽換性、多型的擴充性)
- race condition
Step 1 : 首先先建立一個 ILoggerHelper 介面讓 LoggerHelper 實作
備註 : Resharper 快捷鍵 : Ctrl + R ,I
LoggerHelper 實作 ILoggerHelper 後 Code 如下
Step 2 : 此時原本的 ThirdPartyClass 原本依賴的 static LoggerHelper 方法無法使用會出現 error,如下圖所示
- 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 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類別來代替? 那麼這種手法就是所謂的"隔離"嗎? 因為有聽過這個名詞但不太確定。
謝謝