使用.NET Moq 时如何转发到另一个对象?

How to forward to another object when using .NET Moq?

给定一个对象,我想创建一个实现对象接口并模拟一个方法的模拟,但将其余方法转发给真实对象,不是基础class.

例如:

ISqlUtil sqlUtil = GetTheRealSqlUtilObjectSomehow(...);
var mock = new Mock<ISqlUtil>();
mock.Setup(o => o.SpecialMethodToBeMocked(...)).Returns<...>(...)
// Here I would like to delegate the rest of the methods to the real sqlUtil object. How ?

因此,在示例中,我只想模拟 ISqlUtil.SpecialMethodToBeMocked 并将 methods/properties 的其余部分转发给现有实例 sqlUtil

Moq.NET可以吗?

编辑 1

它也应该适用于泛型方法。

如果默认情况下您无法模拟 class 和委托对基础的调用,则必须手动将委托连接到您的单独实例。

var util = GetSqlUtil();

var mockUtil = new Mock<ISqlUtil>(MockBehavior.Strict);
mockUtil.Setup(x => x.SomeCall(...)).Returns<...>(args => util.SomeCall(args));

开箱即用的最小起订量无法做到这一点。但是,我认为如果您进入下一层并直接使用 Castle DynamicProxy(这是 Moq 下的内容),您基本上可以实现您想要的。

因此,给出以下基本代码来模拟您的问题(本质上是一个接口、一个具体实现和一个工厂,因为具体很难make/setup):

public interface ISqlUtil {
    T SomeGenericMethod<T>(T args);

    int SomeMethodToIntercept();
}
public class ConcreteSqlUtil : ISqlUtil {
    public T SomeGenericMethod<T>(T args){
        return args;
    }
    public int SomeMethodToIntercept() {
        return 42;
    }
}
public class SqlUtilFactory {
    public static ISqlUtil CreateSqlUtil() {
        var rVal = new ConcreteSqlUtil();
        // Some Complex setup
        return rVal;
    }
}

您可以进行以下测试:

public void TestCanInterceptMethods() {
    // Create a concrete instance, using the factory
    var coreInstance = SqlUtilFactory.CreateSqlUtil();

    // Test that the concrete instance works
    Assert.AreEqual(42, coreInstance.SomeMethodToIntercept());
    Assert.AreEqual(40, coreInstance.SomeGenericMethod(40));

    // Create a proxy generator (you'll probably want to put this
    // somewhere static so that it's caching works if you use it)
    var generator = new Castle.DynamicProxy.ProxyGenerator();

    // Use the proxy to generate a new class that implements ISqlUtil
    // Note the concrete instance is passed into the construction
    // As is an instance of MethodInterceptor (see below)
    var proxy = generator.CreateInterfaceProxyWithTarget<ISqlUtil>(coreInstance, 
                                new MethodInterceptor<int>("SomeMethodToIntercept", 33));

    // Check that calling via the proxy still delegates to existing 
    // generic method
    Assert.AreEqual(45, proxy.SomeGenericMethod(45));
    // Check that calling via the proxy returns the result we've specified
    // for our intercepted method
    Assert.AreEqual(33, proxy.SomeMethodToIntercept());
}

方法拦截器如下所示:

public class MethodInterceptor<T> : Castle.DynamicProxy.IInterceptor {
    private T _returns;
    private string _methodName;
    public MethodInterceptor(string methodName, T returns) {
        _returns = returns;
        _methodName = methodName;
    }
    public void Intercept(IInvocation invocation) {
        if (invocation.Method.Name == _methodName) {
            invocation.ReturnValue = _returns;
        }
        else {
            invocation.Proceed();
        }
    }
}

本质上,拦截器检查被调用的方法是否与您感兴趣的方法相匹配,如果是,return存储的 return 值。否则,它调用 Proceed,它将方法调用委托给创建代理时提供的具体对象。

示例代码使用字符串而不是 lambda 来指定拦截方法,显然这可以更改(reader 的练习)。此外,这没有使用 Moq,因此您丢失了 SetupReturnsVerify 元素,这些元素已被拦截器取代,因此这可能与您的内容相去甚远re 之后会有用,但是取决于您的代码的实际情况,它可能是一种可行的替代方法。

成功诱使 Moq 为给定 class 实例 创建代理,我认为调整解决方案很容易您的给定 接口 实现案例。

不行

如果你想到,这是有道理的:接口没有实现。而且由于 Moq 知道模拟类型是一个接口 - 它 does not even try to call the underlying proxy。到此为止,故事结束。

献给不轻易放弃的人

剧透:仍然没有运气

看着library source code,我有一个理论,它可能会强制正确的执行路径:

if (mock.TargetType.IsInterface) // !!! needs to be true here
{
    // !!! we end up here and proceed to `DefaultValueProvider`
}
else
{
    Debug.Assert(mock.TargetType.IsClass); // !!! needs to pass here
    Debug.Assert(mock.ImplementsInterface(declaringType)); // !!! needs to pass here

    // Case 2: Explicitly implemented interface method of a class proxy.
......

为此我们可以满足两个条件:

  1. mock.TargetType 应该是目标 class 实例类型
  2. this.InheritedInterfaces 应该包含我们的接口

第二个很容易搭建:

private void AddInheritedInterfaces(T targetInstance)
{
    var moqAssembly = Assembly.Load(nameof(Moq));
    var mockType = moqAssembly.GetType("Moq.Mock`1");
    var concreteType = mockType.MakeGenericType(typeof(T));
    var fi = concreteType.GetField("inheritedInterfaces", BindingFlags.NonPublic | BindingFlags.Static);
    
    var t = targetInstance.GetType()
        .GetInterfaces()
        .ToArray();
    fi.SetValue(null, t);
}

但据我所知,如果没有 Reflection.Emit 大炮,则无法覆盖标记为 internalMock<>.TargetType 的表达式)属性,其中由于需要大量的覆盖和子 classing,它可能变得不可行 - 在这种情况下,您最好只分叉 Moq 并修补源代码(或者可能提交 PR?)。

可以做什么

应该可以生成 Setup LINQ 表达式,自动调用您各自的实例实现:

//something along these lines, but this is basically sudocode
ISqlUtil sqlUtil = GetTheRealSqlUtilObjectSomehow(...);
var mock = new Mock<ISqlUtil>();
foreach(var methodInfo in typeof(ISqlUtil).GetMembers()) 
{   mock.Setup(Expression.Member(methodInfo)).Returns(Expression.Lambda(Expression.Call(methodInfo)).Compile()())
}

但是考虑到正确解释所有事情需要付出多少努力,这可能也不太可行。