如何在使用 .NET 最小起订量时转发到另一个对象

本文关键字:转发 一个对象 NET | 更新日期: 2023-09-27 18:32:52

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

例如:

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并将其余方法/属性转发到现有实例sqlUtil

Moq.NET 可能吗?

编辑 1

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

如何在使用 .NET 最小起订量时转发到另一个对象

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

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

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

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

示例代码使用字符串而不是 lambda 来指定要拦截的方法,显然这可以更改(读者练习(。此外,这不是使用 Moq,因此您丢失了 SetupReturnsVerify 元素,这些元素被拦截器取代,因此这可能与您追求的内容相去甚远而没有用处,但是根据您的代码真实外观,它可能是一种可行的替代方法。

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

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

在我的另一个 SO 答案中成功诱骗 Moq 为给定的类实例创建代理后,我认为很容易针对给定接口实现的情况调整解决方案。

不可能

如果你想一想,这是有道理的:接口没有实现。而且由于 Moq 知道模拟类型是一个接口 - 它甚至不会尝试调用底层代理。就这样,故事结束了。

对于那些不轻易放弃的人

剧透:还是没有运气

查看库源代码,我有一个理论,即可以强制执行正确的路径:

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应该是目标类实例类型
  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);
}

但据我所知,如果没有大炮,就不可能覆盖标记为 internal 的表达式体属性(Mock<>.TargetType Reflection.Emit(,由于需要覆盖和子类化,它可能变得不可行 - 在这种情况下,您最好只是分叉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()())
}

但考虑到正确解释一切需要付出多少努力,这可能也不是很可行。

有一个

解决方法,即使用本答案中描述的方法@timur。

虽然这种方法不能像他在当前线程中的答案所描述的那样直接在接口上工作,但确实可以通过通用工厂方法做到这一点。

注意:生成的 Moq 对象将不是一个真正的子类,而是一个包装的对象,因此只有public virtual方法将被转发到该对象(与典型的Moq不同,对于非public或非virtual方法/属性,基会自动调用(。

出厂模式如下所示:

static MyMock<T> CreateMock<T>(T target) where T : class, ISqlUtil
{
     var superMock = new MyMock<T>(target); // now we can pass instances!
     superMock.CallBase = true; 
     superMock.Setup(o => o.SpecialMethodToBeMocked(...)).Returns<...>(...);
     return superMock;
}

你像这样使用它:

var mockFunc = typeof(this).GetMethod("CreateMock").MakeGenericMethod(sqlUtil.GetType());
var superMock = mockFunc.Invoke(null, new object[] {sqlUtil}) as Mock;

虽然MyMock的实现将基于本答案中描述的那个(但我正在简化它(。

public class MyMock<T> : Mock<T>, IDisposable where T : class
{
     public MyMock(T targetInstance)
     {
          var moqAssembly = typeof(Mock).Assembly;
          var proxyFactoryType = moqAssembly.GetType("Moq.ProxyFactory");
          var castleProxyFactoryInstance = proxyFactoryType.GetProperty("Instance").GetValue(null);
          var castleProxyFactoryType = moqAssembly.GetType("Moq.CastleProxyFactory");
          var generatorFieldInfo = castleProxyFactoryType.GetField("generator", BindingFlags.NonPublic | BindingFlags.Instance);
         
          generatorFieldInfo.SetValue(castleProxyFactoryInstance, new MyProxyGenerator(targetInstance)); 
     }
}
class MyProxyGenerator : ProxyGenerator
{
   object _target;
   public MyProxyGenerator(object target) {
     _target = target;
}
// this method is 90% taken from the library source. I only had to tweak two lines (see below)
public override object CreateClassProxy(Type classToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options, object[] constructorArguments, params IInterceptor[] interceptors)
{
    if (_target is not null) return CreateClassProxyWithTarget(classToProxy, additionalInterfacesToProxy, _target, options, constructorArguments, interceptors);
    return base.CreateClassProxy(classToProxy, additionalInterfacesToProxy,  options, constructorArguments, interceptors);
}