如何使用 MOQ 框架在 c# 中模拟静态方法?

Posted

技术标签:

【中文标题】如何使用 MOQ 框架在 c# 中模拟静态方法?【英文标题】:How to mock static methods in c# using MOQ framework? 【发布时间】:2012-09-16 19:08:04 【问题描述】:

我最近一直在做单元测试,我已经使用 MOQ 框架和 MS Test 成功地模拟了各种场景。我知道我们无法测试私有方法,但我想知道我们是否可以使用 MOQ 模拟静态方法。

【问题讨论】:

【参考方案1】:

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方法来返回你自己的测试数据。

希望对您有所帮助。

【讨论】:

我相信这被称为适配器模式。 顺便说一句,这种模式称为提取和覆盖【参考方案2】:

另一种将静态方法转换为静态 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);
    

为每个静态类添加单独的一行。这样做是,在单元测试完成运行后,它将所有静态字段重置为其原始值。这样,同一项目中的其他单元测试将从正确的默认值开始,而不是您的模拟版本。

【讨论】:

另外,还有一件事要记住。当您像这样设置静态变量时,您应该在测试完成后撤消它。一种方法是重新运行类的静态初始化程序。示例:typeof(SomeClassName).TypeInitializer.Invoke(null, null); 这太棒了。太感谢了。这么小的变化,这么大的好处。我想拥抱你。 这是迄今为止我最喜欢的方法。将所有内容放入非静态类中,以便您可以模拟该方法,这对我来说是一种巨大的代码气味。特别是当你最终得到这样的代码时:var people = new PersonRepository().GetAll();。创建 PersonRepository 在这里没有用处。 请注意,任何消费类都可以覆盖这个“方法”,不仅仅是你的单元测试!为什么这很关键? public static Func&lt;string, bool&gt; IsSecredPasswordValid .... 可以很容易地被覆盖以始终返回 true。至少将Func 声明为internal 并使用InternalsVisibleTo 属性来只允许您的单元测试是一种更好的安全性。 在并行运行测试时更改静态属性可能会导致问题。【参考方案3】:

Moq 不能模拟类的静态成员。

在为可测试性设计代码时,避免静态成员(和单例)很重要。一种可以帮助您重构代码以提高可测试性的设计模式是依赖注入。

这意味着改变这个:

public class Foo

    public Foo()
    
        Bar = new Bar();
    

public Foo(IBar bar)

    Bar = bar;

这允许您使用单元测试中的模拟。在生产环境中,您可以使用 Ninject 或 Unity 之类的依赖注入工具,它们可以将所有内容连接在一起。

我前段时间写了一篇关于这个的博客。它解释了哪些模式可用于更好的可测试代码。或许对你有帮助:Unit Testing, hell or heaven?

另一种解决方案是使用Microsoft Fakes Framework。这不能替代编写设计良好的可测试代码,但它可以帮助您。 Fakes 框架允许您模拟静态成员并在运行时用您自己的自定义行为替换它们。

【讨论】:

【参考方案4】:

正如其他答案中提到的,MOQ 不能模拟静态方法,作为一般规则,应尽可能避免使用静态方法。

有时这是不可能的。一种是使用遗留代码或第 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 属性设置为我们的模拟。使用这种风格减少了要注入的接口数量,但使依赖关系变得不那么明显(尽管我认为不会比原来的静态调用更明显)。

【讨论】:

【参考方案5】:

我们通常通过依赖于接口之类的抽象来模拟实例(非静态)类及其方法,而不是直接依赖于具体类。

我们可以用静态方法做同样的事情。这是一个依赖于静态方法的类的示例。 (这太做作了。)在这个例子中,我们直接依赖于静态方法,所以我们不能模拟它。

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&lt;int, int, long&gt; 或委托,而不是注入接口。两者都可以,但我更喜欢委托,因为我们可以给它起一个名称,说明它的用途,并将它与具有相同签名的其他函数区分开来。

这是委托以及我们注入委托时类的样子:


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);

【讨论】:

以上是关于如何使用 MOQ 框架在 c# 中模拟静态方法?的主要内容,如果未能解决你的问题,请参考以下文章

c#单元测试:使用Moq框架Mock对象

C#单元测试--如何使用moq.mock进行依赖注入

使用Moq模拟实体框架6 ObjectResult

使用 Moq 模拟类时,如何仅针对特定方法进行 CallBase?

如何使用 Moq 在 .NET Core 2.1 中模拟新的 HttpClientFactory

如何安装起订量框架