只有累積,沒有奇蹟

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 如下
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
需要針對此測試方法進行解耦的設計讓 ThirdPartyClass 與 LoggerHelper 都依賴於某物件互相不直接耦合 以達到解耦合的效果過去有學到很多種方法可以達到這件事情,本次介紹的是透過 interface 來解決這問題,說明如下

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
Step 4 : 在測試建立一個 fakeLoggerHelper 實作 ILoggerHelper 介面,並產生相對應實作方法 Implement  missing member,接著 ThirdPartyClassLog 建構子注入 fakeLoggerHelperCode 如下
[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會讓自己對單元測試了解更多,但建議還是要搭配實務才可以驗證到底自己是否真正了解,否則久了沒用(老了?)有一天還是容易忘記

參考

1 則留言:

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

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

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

    謝謝

    回覆刪除

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

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com