Moq模拟扩展方法


174

我已有一个接口...

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调用?


3
以我的经验,术语混合和扩展方法是分开的。在这种情况下,我将使用后者来避免混淆:P
Ruben Bartelink 2010年

Answers:


33

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

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

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

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


2
除Moles外,还有其他(非免费)模拟框架使用.NET的探查器API来模拟对象,因此可以替换任何调用。我知道的两个是Telerik的JustMockTypeMock Isolator
Marcel Gosselin

6
从理论上讲摩尔是好的,但是我在试用它时发现了三个问题,这使我停止使用它了……1)它在Resharper NUnit运行器中没有运行2)您需要为每个残存的组件手动创建一个摩尔组件3 )无论何时更改存根方法,您都需要手动重新创建摩尔组件。
罗素·吉丁斯

26

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

请参阅Paul Irwin的《模拟静态方法进行单元测试》,其中有很好的示例。


12
我喜欢这个答案,因为它要说的是(无需直接说出)您需要更改代码以使其可测试。这就是它的工作原理。理解在微芯片/ IC / ASIC设计中,这些芯片不仅必须设计为可以工作,而且还必须进一步设计以进行测试,因为如果您无法测试微芯片,那么它就没用了-您无法保证它会工作。软件也是如此。如果您尚未将其构建为可测试的,则它是……无用的。将其构建为可测试的,在某些情况下,这意味着重写代码(并使用包装器),然后构建用于对其进行测试的自动化测试。
迈克尔·普劳兹

2
我创建了一个包装Dapper,Dapper.Contrib和IDbConnection的小型库。github.com/codeapologist/DataAbstractions.Dapper
Drew Sumido

15

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

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


11

您可以模拟一个测试接口,该接口继承自真实接口,并且具有与扩展方法具有相同签名的成员。

然后,您可以模拟测试接口,将真实的接口添加到模拟接口中,并在设置中调用测试方法。

然后,您的模拟实现可以调用您想要的任何方法,或仅检查该方法是否被调用:

IReal //on which some extension method is defined
{
    ... SomeRegularMethod(...);
}

static ExtensionsForIReal
{
    static ... SomeExtensionMethod(this IReal iReal,...);
}

ITest: IReal
{
    //This is a regular method with same name and signature as the extension without the "this IReal iReal" parameter
    ... SomeExtensionMethod(...);
}

var someMock = new Mock<ITest>();
Mock.As<IReal>(); //ad IReal to the mock
someMock.Setup(x => x.SomeExtensionMethod(...)).Verifiable(); //Calls SomeExtensionMethod on ITest
someMock.As<IReal>().Setup(x => x.SomeRegularMethod(...)).Verifiable(); //Calls SomeRegularMethod on IReal

由于在哈佛氏液这篇文章对如何实现支持两种接口模拟。一旦找到它,就可以通过测试界面和静态方法对其进行调整了。


你能告诉我们,请同样本签名SomeNotAnExtensionMethodSomeNotAnExtensionMethod?我现在已经知道如何在接口内创建扩展方法签名...
Peter Csala

1
@Peter Csala:很抱歉,如果不清楚。我更新了帖子以使其更清晰,并将SomeNotAnExtensionMethod重命名为SomeRegularMethod。
pasx

如何在情况下将iReal参数传递给?如果将其作为第一个参数传递,那么如何设置?这将导致运行时异常。SomeExtensionMethodITestSetup( x=> x.SomeExtensionMethod(x, ...)
Peter Csala

你没有通过。从模拟的角度来看,ITest包含一个不带this参数的常规方法,以匹配代码中对扩展方法的调用签名,因此您在此永远不会收到IReal,无论如何,IReal / ITest的实现都是模拟本身。如果要在SomeExtensionMethod中访问模拟的IReal的某些属性,则应在模拟中完成所有操作,例如:object _mockCache = whatever...`Setup(x => x.SomeExtensionMethod(...).. Callback(()=>访问_mockCache此处);)
pasx

8

您可以使用JustMock轻松模拟扩展方法。该API与模拟正常方法相同。考虑以下

public static string Echo(this Foo foo, string strValue) 
{ 
    return strValue; 
}

要安排和验证此方法,请使用以下方法:

string expected = "World";

var foo = new Foo();
Mock.Arrange(() => foo.Echo(Arg.IsAny<string>())).Returns(expected);

string result = foo.Echo("Hello");

Assert.AreEqual(expected, result);

这也是文档的链接:扩展方法模拟


2

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

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

    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设置为静态来解决此问题。不利的一面是,如果同时运行单元测试,则可能会完成两个可能修改相同静态值的不同单元测试。
zumalifeguard '16

-1

因此,如果您使用的是Moq,并且想模拟扩展方法的结果,则可以使用 SetupReturnsDefault<ReturnTypeOfExtensionMethod>(new ConcreteInstanceToReturn())在具有您要模拟的扩展方法的模拟类的实例上。

它不是完美的,但是对于单元测试而言,它运行良好。


我相信它是SetReturnsDefault <T>()
David

永远不会返回具体实例。如果是自定义C#类,则为null!
HelloWorld
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.