使用Moq模拟扩展方法

我有一个预先存在的接口…

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

184096 次浏览

你不能“直接”;用模拟框架模拟静态方法(即扩展方法)。你可以试试mole (http://research.microsoft.com/en-us/projects/pex/downloads.aspx),这是微软提供的一个免费工具,它实现了不同的方法。 下面是工具的描述:

mole是一个基于委托的。net测试存根和弯路的轻量级框架。

mole可以用来绕过任何. net方法,包括密封类型中的非虚拟/静态方法。

你可以在任何测试框架中使用mole(这是独立的)。

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

参见Paul Irwin的模拟单元测试的静态方法,它有很好的例子。

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

我使用了Action、Func、Predicate或delegate类型的内部Lazy Injectable Property,并允许在单元测试期间注入(交换)方法。

    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/

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

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

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

它不是完美的,但是对于单元测试来说,它工作得很好。

如果你只是想确保扩展方法被调用,并且你不想设置一个返回值,那么你可以检查被模拟对象的Invocations属性。

是这样的:

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

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

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

在我的例子中,扩展方法是围绕我的类的一些公共方法的方法。我检查了内部方法的调用。这种方法类似于Alvis的答案(上面)。