簡體   English   中英

使用 .NET Moq 時如何轉發到另一個對象?

[英]How to forward to another object when using .NET Moq?

給定一個對象,我想創建一個實現對象接口並模擬一個方法的模擬,但將其余方法轉發給真實對象,而不是基類

例如:

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

它也應該適用於泛型方法。

開箱即用的 Moq 無法做到這一點。 但是,我認為如果您進入下一層並直接使用 Castle DynamicProxy(這是 Moq 下面的內容),您基本上可以實現您想要的。

因此,給定以下基本代碼來模擬您的問題(本質上是一個接口、一個具體實現和一個工廠,因為具體很難制作/設置):

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 ,它將方法調用委托給創建代理時提供的具體對象。

示例代碼使用字符串而不是 lambdas 來指定要攔截的方法,顯然這可以更改(讀者練習)。 此外,這不使用 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);
}

但據我所知,如果沒有Reflection.Emit火炮,覆蓋標記為internal的表達式主體屬性( Mock<>.TargetType是)是不可能的,由於需要大量覆蓋和子類化,它可能會變得不可行 - 你在這種情況下,最好只分叉起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);
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM