情境
由於部門過去的 專案幾乎都沒有加上單元測試進行保護,主管在新的一年規劃中開發代碼更有品質,希望開發的專案加上新功能或是修改時要加上單元測試,有些 Legacy Code 寫法是屬於一條龍式的 「義大利麵式碼」特別有親切感(大誤,遇到這種就需要先重構 (refactor) 後物件化比較好寫單元測試,今天主題是單元測試中要被測試的 method 中常常會有相依於某個 Static method,在寫 Code 時用 static 物件寫起來很方便,但遇到這種情況在單元測試就需要多下點功夫,以下簡單筆記如何測試 static class 的方法
解決方案
舉個例子來說有個需求要去呼叫第三方取得相關資訊,定義一個 ThirdParty 類別其中透過 SendRequest 方法來送出 Request 與第三方 API 溝通,使用 static 的 LoggerHelper 記錄第三方回傳的內容 response 物件,其中是 Legacy Code 紀錄 log 方式是 Log message 寫到網路硬碟,Sample Code 如下
Step 1 : 首先先建立一個 ILoggerHelper 介面讓 LoggerHelper 實作
備註 : Resharper 快捷鍵 : Ctrl + R ,I
LoggerHelper 實作 ILoggerHelper 後 Code 如下
由於部門過去的 專案幾乎都沒有加上單元測試進行保護,主管在新的一年規劃中開發代碼更有品質,希望開發的專案加上新功能或是修改時要加上單元測試,有些 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會讓自己對單元測試了解更多,但建議還是要搭配實務才可以驗證到底自己是否真正了解,否則久了沒用















