使用 Moq 模拟扩展方法

Posted

技术标签:

【中文标题】使用 Moq 模拟扩展方法【英文标题】:Mocking Extension Methods with Moq 【发布时间】:2011-01-18 18:03:24 【问题描述】:

我有一个预先存在的界面...

public interface ISomeInterface

    void SomeMethod();

我已经使用 mixin 扩展了这个接口...

public static class SomeInterfaceExtensions

    public static void AnotherMethod(this ISomeInterface someInterface)
    
        // Implementation here
    

我有一个班级,我想测试它的名字......

public class Caller

    private readonly ISomeInterface someInterface;

    public Caller(ISomeInterface someInterface)
    
        this.someInterface = someInterface;
    

    public void Main()
    
        someInterface.AnotherMethod();
    

还有一个我想模拟接口并验证对扩展方法的调用的测试...

    [Test]
    public void Main_BasicCall_CallsAnotherMethod()
    
        // Arrange
        var someInterfaceMock = new Mock<ISomeInterface>();
        someInterfaceMock.Setup(x => x.AnotherMethod()).Verifiable();

        var caller = new Caller(someInterfaceMock.Object);

        // Act
        caller.Main();

        // Assert
        someInterfaceMock.Verify();
    

但运行此测试会产生异常...

System.ArgumentException: Invalid setup on a non-member method:
x => x.AnotherMethod()

我的问题是,有没有一种模拟 mixin 调用的好方法?

【问题讨论】:

根据我的经验,术语 mixin 和扩展方法是分开的。在这种情况下,我会使用后者来避免混淆:P 重复:***.com/questions/562129/…. 【参考方案1】:

我使用 Wrapper 来解决这个问题。创建一个包装对象并传递您的模拟方法。

参见 Paul Irwin 的 Mocking Static Methods for Unit Testing,它有很好的例子。

【讨论】:

我喜欢这个答案,因为它所说的(没有直接说)是您需要更改代码以使其可测试。这就是它的工作原理。了解在微芯片/IC/ASIC 设计中,这些芯片不仅要设计成可以工作,而且还要进一步设计成可测试的,因为如果你不能测试微芯片,它就没有用——你不能保证它会工作。软件也是如此。如果您还没有将其构建为可测试的,那么它是……没用的。将其构建为可测试的,这在某些情况下意味着重写代码(并使用包装器),然后构建测试它的自动化测试。 我创建了一个包含 Dapper、Dapper.Contrib 和 IDbConnection 的小型库。 github.com/codeapologist/DataAbstractions.Dapper【参考方案2】:

您不能使用模拟框架“直接”模拟静态方法(因此是扩展方法)。您可以尝试 Moles (http://research.microsoft.com/en-us/projects/pex/downloads.aspx),这是一个来自 Microsoft 的免费工具,它实现了不同的方法。 以下是该工具的说明:

Moles 是一个轻量级框架,用于 .NET 中基于委托的测试存根和绕行。

Moles 可用于绕过任何 .NET 方法,包括密封类型中的非虚拟/静态方法。

您可以将 Moles 与任何测试框架一起使用(它是独立的)。

【讨论】:

除了 Moles,还有其他(非免费)模拟框架使用 .NET 的分析器 API 来模拟对象,因此可以替换任何调用。我认识的两个是Telerik's JustMock 和TypeMock Isolator。 Moles 理论上是好的,但我在试用时发现了三个问题,阻止了我使用它... 1)它不能在 Resharper NUnit 运行器中运行 2)您需要手动创建每个 stubbed 程序集都有一个 moole 程序集 3) 每当 stubbed 方法发生变化时,您都需要手动重新创建一个 moole 程序集。【参考方案3】:

我发现我必须发现我试图模拟输入的扩展方法的内部,并模拟扩展内部发生的事情。

我认为使用扩展程序直接将代码添加到您的方法中。这意味着我需要模拟扩展内部发生的事情,而不是扩展本身。

【讨论】:

这并不总是可行的。例如。使用不是您编写的非开源扩展时。【参考方案4】:

当我包装对象本身时,我喜欢使用包装器(适配器模式)。我不确定我是否会用它来包装扩展方法,它不是对象的一部分。

我使用 Action、Func、Predicate 或委托类型的内部惰性可注入属性,并允许在单元测试期间注入(换出)方法。

    internal Func<IMyObject, string, object> DoWorkMethod
    
        [ExcludeFromCodeCoverage]
        get  return _DoWorkMethod ?? (_DoWorkMethod = (obj, val) =>  return obj.DoWork(val); ); 
        set  _DoWorkMethod = value; 
     private Func<IMyObject, string, object> _DoWorkMethod;

然后你调用 Func 而不是实际的方法。

    public object SomeFunction()
    
        var val = "doesn't matter for this example";
        return DoWorkMethod.Invoke(MyObjectProperty, val);
    

如需更完整的示例,请查看http://www.rhyous.com/2016/08/11/unit-testing-calls-to-complex-extension-methods/

【讨论】:

这很好,但读者应该知道_DoWorkMethod 是该类的一个新字段,现在该类的每个实例都必须再分配一个字段。这很少见,但有时它确实取决于您在任何时候分配的实例数量。您可以通过将 _DoWorkMethod 设为静态来解决此问题。这样做的缺点是,如果您同时运行单元测试,则可能会修改相同静态值的两个不同单元测试将完成。 非常精简和整洁【参考方案5】:

如果您只是想确保调用了扩展方法,而不是尝试设置返回值,那么您可以检查模拟对象的 Invocations 属性。

像这样:

var invocationsCount = mockedObject.Invocations.Count;
invocationsCount.Should().BeGreaterThan(0);

【讨论】:

对于返回值,我们使用了起订量的回调功能【参考方案6】:

无法模拟扩展方法的原因已经在很好的答案中给出。我只是想用这个答案给出另一种可能的解决方案:通过调用扩展方法提取受保护的虚拟方法,并使用代理在测试类/方法中为此方法创建设置。

public class Foo

  public void Method()
    => CallToStaticMethod();

  protected virtual void CallToStaticMethod()
    => StaticClass.StaticMethod();

测试

[TestMethod]
    public void MyTestMethod()
    
        var expected = new Exception("container exception");
        var proxy = new Mock<Foo>();
        proxy.Protected().Setup("CallToStaticMethod").Throws(expected);

        var actual = Assert.ThrowsException<Exception>(() => proxy.Object.Foo());

        Assert.AreEqual(expected, actual);
    
  

【讨论】:

【参考方案7】:

因此,如果您正在使用 Moq,并且想要模拟扩展方法的结果,那么您可以在具有您尝试模拟的扩展方法的模拟类的实例上使用 SetupReturnsDefault&lt;ReturnTypeOfExtensionMethod&gt;(new ConcreteInstanceToReturn())

它并不完美,但出于单元测试的目的,它运行良好。

【讨论】:

我相信是 SetReturnsDefault() 永远不会返回具体实例。如果是自定义 c# 类,则为 null! 举个例子就好了。我不能让它工作。

以上是关于使用 Moq 模拟扩展方法的主要内容,如果未能解决你的问题,请参考以下文章

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

使用 Moq 模拟单元测试的异步方法

Moq:设置一个模拟方法在第一次调用时失败,在第二次调用时成功

使用Moq模拟IList.Add

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

使用 moq 模拟虚拟只读属性