簡體   English   中英

如何使用 MOQ 框架在 c# 中模擬靜態方法?

[英]How to mock static methods in c# using MOQ framework?

我最近一直在做單元測試,我已經成功地使用 MOQ 框架和 MS 測試模擬了各種場景。 我知道我們不能測試私有方法,但我想知道我們是否可以使用最小起訂量模擬靜態方法。

Moq(和其他基於DynamicProxy的模擬框架)無法模擬任何不是虛擬或抽象方法的東西。

密封/靜態類/方法只能使用基於 Profiler API 的工具來偽造,例如Typemock (商業)或 Microsoft Moles(免費,在 Visual Studio 2012 Ultimate /2013 /2015 中稱為Fakes )。

或者,您可以重構您的設計以抽象調用靜態方法,並通過依賴注入將此抽象提供給您的類。 那么您不僅會有更好的設計,而且可以使用免費工具(如 Moq)進行測試。

無需完全使用任何工具即可應用允許可測試性的通用模式。 考慮以下方法:

public class MyClass
{
    public string[] GetMyData(string fileName)
    {
        string[] data = FileUtil.ReadDataFromFile(fileName);
        return data;
    }
}

您可以將其包裝在protected virtual方法中,而不是嘗試模擬FileUtil.ReadDataFromFile ,如下所示:

public class MyClass
{
    public string[] GetMyData(string fileName)
    {
        string[] data = GetDataFromFile(fileName);
        return data;
    }

    protected virtual string[] GetDataFromFile(string fileName)
    {
        return FileUtil.ReadDataFromFile(fileName);
    }
}

然后,在您的單元測試中,從MyClass派生並將其命名為TestableMyClass 然后您可以覆蓋GetDataFromFile方法以返回您自己的測試數據。

希望有幫助。

將靜態方法轉換為靜態 Func 或 Action 的另一種選擇。 例如。

原始代碼:

    class Math
    {
        public static int Add(int x, int y)
        {
            return x + y;
        }

您想“模擬” Add 方法,但不能。 把上面的代碼改成這樣:

        public static Func<int, int, int> Add = (x, y) =>
        {
            return x + y;
        };

現有的客戶端代碼不必更改(可能重新編譯),但源代碼保持不變。

現在,從單元測試中,要更改方法的行為,只需為其重新分配一個內聯函數:

    [TestMethod]
    public static void MyTest()
    {
        Math.Add = (x, y) =>
        {
            return 11;
        };

將您想要的任何邏輯放入方法中,或者僅返回一些硬編碼值,具體取決於您要執行的操作。

這可能不一定是您每次都可以做的事情,但在實踐中,我發現這種技術效果很好。

[編輯] 我建議您將以下清理代碼添加到您的單元測試類:

    [TestCleanup]
    public void Cleanup()
    {
        typeof(Math).TypeInitializer.Invoke(null, null);
    }

為每個靜態類添加單獨的一行。 這樣做是,在單元測試完成運行后,它將所有靜態字段重置為其原始值。 這樣,同一項目中的其他單元測試將以正確的默認值開始,而不是您的模擬版本。

Moq 不能模擬類的靜態成員。

在為可測試性設計代碼時,避免靜態成員(和單例)很重要。 一種可以幫助您重構代碼以提高可測試性的設計模式是依賴注入。

這意味着改變這一點:

public class Foo
{
    public Foo()
    {
        Bar = new Bar();
    }
}

public Foo(IBar bar)
{
    Bar = bar;
}

這允許您使用單元測試中的模擬。 在生產中,您使用像NinjectUnity這樣的依賴注入工具,可以將所有東西連接在一起。

我前段時間寫了一篇關於這個的博客。 它解釋了哪些模式可用於更好的可測試代碼。 也許它可以幫助你: 單元測試,地獄還是天堂?

另一種解決方案可能是使用Microsoft Fakes Framework 這不能替代編寫設計良好的可測試代碼,但它可以幫助您。 Fakes 框架允許您模擬靜態成員並在運行時用您自己的自定義行為替換它們。

正如其他答案中提到的那樣,最小起訂量不能模擬靜態方法,作為一般規則,應該盡可能避免使用靜態方法。

有時這是不可能的。 一種是使用遺留代碼或第 3 方代碼,甚至使用靜態的 BCL 方法。

一種可能的解決方案是使用可以模擬的接口將靜態包裝在代理中

    public interface IFileProxy {
        void Delete(string path);
    }

    public class FileProxy : IFileProxy {
        public void Delete(string path) {
            System.IO.File.Delete(path);
        }
    }

    public class MyClass {

        private IFileProxy _fileProxy;

        public MyClass(IFileProxy fileProxy) {
            _fileProxy = fileProxy;
        }

        public void DoSomethingAndDeleteFile(string path) {
            // Do Something with file
            // ...
            // Delete
            System.IO.File.Delete(path);
        }

        public void DoSomethingAndDeleteFileUsingProxy(string path) {
            // Do Something with file
            // ...
            // Delete
            _fileProxy.Delete(path);

        }
    }

不利的一面是,如果有很多代理,ctor 可能會變得非常混亂(盡管可以說,如果有很多代理,那么該類可能會嘗試做太多事情並且可以重構)

另一種可能性是擁有一個“靜態代理”,其背后有不同的接口實現

   public static class FileServices {

        static FileServices() {
            Reset();
        }

        internal static IFileProxy FileProxy { private get; set; }

        public static void Reset(){
           FileProxy = new FileProxy();
        }

        public static void Delete(string path) {
            FileProxy.Delete(path);
        }

    }

我們的方法現在變成

    public void DoSomethingAndDeleteFileUsingStaticProxy(string path) {
            // Do Something with file
            // ...
            // Delete
            FileServices.Delete(path);

    }

為了測試,我們可以將 FileProxy 屬性設置為我們的模擬。 使用這種風格減少了要注入的接口數量,但使依賴關系不那么明顯(盡管不比我想的原始靜態調用更明顯)。

我們通常通過依賴於接口之類的抽象來模擬實例(非靜態)類及其方法,而不是直接依賴於具體類。

我們可以對靜態方法做同樣的事情。 這是一個依賴於靜態方法的類的示例。 (這是非常人為的。)在這個例子中,我們直接依賴於靜態方法,所以我們不能模擬它。

public class DoesSomething
{
    public long AddNumbers(int x, int y)
    {
        return Arithemetic.Add(x, y); // We can't mock this :(
    }
}

public static class Arithemetic
{
    public static long Add(int x, int y) => x + y;
}

為了能夠模擬Add方法,我們可以注入一個抽象。 我們可以注入Func<int, int, long>或委托,而不是注入接口。 兩者都可以,但我更喜歡委托,因為我們可以給它起一個名稱,說明它的用途,並將它與具有相同簽名的其他函數區分開來。

這是委托以及注入委托時類的外觀:

public delegate long AddFunction(int x, int y);

public class DoesSomething
{
    private readonly AddFunction _addFunction;

    public DoesSomething(AddFunction addFunction)
    {
        _addFunction = addFunction;
    }

    public long AddNumbers(int x, int y)
    {
        return _addFunction(x, y);
    }
}

這與我們將接口注入類的構造函數時的工作方式完全相同。

我們可以使用 Moq 為委托創建模擬,就像我們使用接口一樣。

var addFunctionMock = new Mock<AddFunction>();
addFunctionMock.Setup(_ => _(It.IsAny<int>(), It.IsAny<int>())).Returns(2);
var sut = new DoesSomething(addFunctionMock.Object);

...但是這種語法冗長且難以閱讀。 我不得不谷歌它。 如果我們使用匿名函數而不是 Moq 會容易得多:

AddFunction addFunctionMock = (x, y) => 2;
var sut = new DoesSomething(addFunctionMock);

我們可以使用任何具有正確簽名的方法。 如果我們願意,我們可以使用該簽名在我們的測試類中定義另一個方法並使用它。


順便說一句,如果我們注入一個委托,我們如何使用我們的 IoC 容器進行設置? 它看起來就像注冊一個接口和實現。 使用IServiceCollection

serviceCollection.AddSingleton<AddFunction>(Arithemetic.Add);

遵循@manojlds 的建議

Moq(和 NMock、RhinoMock)不會在這里幫助你。 您將必須圍繞 LogException 創建一個包裝類(和虛擬方法)並在生產代碼中使用它並使用它進行測試。

這是我想分享的部分解決方案。

我遇到了與此類似的問題,我實施了以下解決方案。

問題

帶有靜態方法的原始類

該類也可以是靜態的。

public class LogHelper
{
    public static string LogError(Exception ex, string controller, string method)
    {
        // Code
    }

    public static string LogInfo(string message, string controller, string method)
    {
        // Code
    }

    public static Logger Logger(string logId, string controller, string method)
    {
        // Code
    }
}

你不能直接模擬這個,但你可以通過一些接口來實現。

解決方案

界面

請注意,該接口定義了該類的所有靜態方法。

public interface ILogHelperWrapper
{
    string LogError(Exception ex, string controller, string method);
    string LogInfo(string message, string controller, string method);
    Logger Logger(string logId, string controller, string method);
}

然后,在類包裝器中實現這個接口。

包裝類

public class LogHelperWrapper : ILogHelperWrapper
{
    public string LogError(Exception ex, string controller, string method)
    {
        return LogHelper.LogError(ex, controller, method);            
    }
    public string LogInfo(string message, string controller, string method)
    {
        return LogHelper.LogInfo(message, controller, method);
    }

    public Logger Logger(string logId, string controller, string method)
    {
        return LogHelper.Logger(logId, controller, method);
    }
}

這樣你就可以模擬LogHelper的靜態方法。

從單元測試中模擬

  • 這個接口是可模擬的,實現它的類可以在生產中使用(雖然我還沒有在生產中運行它,但它在開發期間工作)。
  • 然后,例如,我可以調用靜態方法LogError
public void List_ReturnList_GetViewResultWithList()
{
    // Arrange
    var mockLogHelper = new Mock<ILogHelperWrapper>();
    
    mockLogHelper.Setup(helper => helper.LogError(new Exception(), "Request", "List")).Returns("Some Returned value");

    var controller = new RequestController(mockLogHelper.Object);

    // Act
    var actual = controller.DisplayList();

    // Assert
    Assert.IsType<ViewResult>(actual);
}

筆記

正如我之前所說,這是一個部分解決方案,我仍在實施。 我正在將其作為社區維基進行檢查。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM