Создание Mock с Moq вокруг существующего экземпляра? - PullRequest
1 голос
/ 09 января 2020

Допустим, у нас есть интеграционный тест с расширенной конфигурацией IConfiguration. Я настроил тест для работы с контейнерами autofa c, и теперь я хотел бы использовать Mock для замены операции над одним из его свойств без необходимости имитировать или заменять все остальное:

var config = MyTestContainer.Resolve<IConfiguration>();
//let's say that config.UseFeatureX = false;

//here, I'd like to create mock "around" the existing instance:
var mockedConfig = Mock.CreateWith(config);  //CreateWith => a method I'd like to find how to do
mockedConfig.Setup(c => c.UseFeatureX).Returns(true);

Как сделать это обтекание существующего экземпляра? Это должно быть похоже на .CallBase, но вместо того, чтобы просто вызывать базовую реализацию, я надеялся, что будет способ вызвать базовые значения.

1 Ответ

1 голос
/ 09 января 2020

Я считаю, что по умолчанию Moq позволяет передавать параметры конструктора для реализации IConfiguration, и он создаст для вас новый экземпляр этого класса. Если я правильно понимаю вашу проблему, вы хотите использовать предварительно созданный экземпляр. Я предполагаю, что вы знаете о CallBase, и он не совсем делает то, что вам нужно.

Таким образом, следующий фрагмент иллюстрирует проблему:

//suppose we've got a class:
public class A
{
    public string Test {get;set;}
    public virtual string ReturnTest() => Test;
}
//and some code below:
void Main()
{
    var config = new A() {
        Test = "TEST"
    } ;

    var mockedConfig = new Mock<A>(); // first we run a stock standard mock
    mockedConfig.CallBase = true; // we will enable CallBase just to point out that it makes no difference  
    var o = mockedConfig.Object;
    Console.WriteLine(o.ReturnTest()); // this will be null because Test has not been initialised from constructor
    mockedConfig.Setup(c => c.ReturnTest()).Returns("mocked"); // of course if you set up your mocks - you will get the value
    Console.WriteLine(o.ReturnTest()); // this will be "mocked" now, no surprises
}

теперь, зная, что Moq внутренне использует Castle DynamicProxy , и это фактически позволяет нам генерировать прокси для экземпляров (они называют это Класс прокси с целью ). Поэтому вопрос в том, как нам заставить Moq сделать его для нас. Кажется, что нет такой опции из коробки, и простое внедрение переопределения не вполне go, а также не так много инверсии управления внутри библиотеки, и большинство типов и свойств помечены как internal, что делает наследование практически невозможно.

Castle Proxy, однако, гораздо более дружественен к пользователю и имеет довольно много открытых методов и доступных для переопределения. Итак, давайте определим класс ProxyGenerator, который будет принимать вызовы метода Moq и добавить к нему необходимые функциональные возможности (просто сравните реализации CreateClassProxyWithTarget и CreateClassProxy - они почти идентичны!)

MyProxyGenerator.cs

class MyProxyGenerator : ProxyGenerator
{
    object _target;

    public MyProxyGenerator(object target) {
        _target = target; // this is the missing piece, we'll have to pass it on to Castle proxy
    }
    // 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 (classToProxy == null)
        {
            throw new ArgumentNullException("classToProxy");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }
        if (!classToProxy.GetTypeInfo().IsClass)
        {
            throw new ArgumentException("'classToProxy' must be a class", "classToProxy");
        }
        CheckNotGenericTypeDefinition(classToProxy, "classToProxy");
        CheckNotGenericTypeDefinitions(additionalInterfacesToProxy, "additionalInterfacesToProxy");
        Type proxyType = CreateClassProxyTypeWithTarget(classToProxy, additionalInterfacesToProxy, options); // these really are the two lines that matter
        List<object> list =  BuildArgumentListForClassProxyWithTarget(_target, options, interceptors);       // these really are the two lines that matter
        if (constructorArguments != null && constructorArguments.Length != 0)
        {
            list.AddRange(constructorArguments);
        }
        return CreateClassProxyInstance(proxyType, list, classToProxy, constructorArguments);
    }
}

, если все вышеперечисленное было относительным простым, то фактическая подача его в Moq будет чем-то вроде взлома. Как я уже упоминал, большинство структур помечены internal, поэтому нам придется использовать отражение, чтобы пройти:

MyMock.cs

public class MyMock<T> : Mock<T>, IDisposable where T : class
{
    void PopulateFactoryReferences()
    {
        // Moq tries ridiculously hard to protect their internal structures - pretty much every class that could be of interest to us is marked internal
        // All below code is basically serving one simple purpose = to swap a `ProxyGenerator` field on the `ProxyFactory.Instance` singleton
        // all types are internal so reflection it is
        // I will invite you to make this a bit cleaner by obtaining the `_generatorFieldInfo` value once and caching it for later
        var moqAssembly = Assembly.Load(nameof(Moq));
        var proxyFactoryType = moqAssembly.GetType("Moq.ProxyFactory");
        var castleProxyFactoryType = moqAssembly.GetType("Moq.CastleProxyFactory");     
        var proxyFactoryInstanceProperty = proxyFactoryType.GetProperty("Instance");
        _generatorFieldInfo = castleProxyFactoryType.GetField("generator", BindingFlags.NonPublic | BindingFlags.Instance);     
        _castleProxyFactoryInstance = proxyFactoryInstanceProperty.GetValue(null);
        _originalProxyFactory = _generatorFieldInfo.GetValue(_castleProxyFactoryInstance);//save default value to restore it later
    }

    public MyMock(T targetInstance) {       
        PopulateFactoryReferences();
        // this is where we do the trick!
        _generatorFieldInfo.SetValue(_castleProxyFactoryInstance, new MyProxyGenerator(targetInstance));
    }

    private FieldInfo _generatorFieldInfo;
    private object _castleProxyFactoryInstance;
    private object _originalProxyFactory;

    public void Dispose()
    {
         // you will notice I opted to implement IDisposable here. 
         // My goal is to ensure I restore the original value on Moq's internal static class property in case you will want to mix up this class with stock standard implementation
         // there are probably other ways to ensure reference is restored reliably, but I'll leave that as another challenge for you to tackle
        _generatorFieldInfo.SetValue(_castleProxyFactoryInstance, _originalProxyFactory);
    }
}

, учитывая, что мы получили выше работая, реальное решение будет выглядеть так:

    var config = new A()
    {
        Test = "TEST"
    };
    using (var superMock = new MyMock<A>(config)) // now we can pass instances!
    {
        superMock.CallBase = true; // you still need this, because as far as Moq is oncerned it passes control over to CastleDynamicProxy   
        var o1 = superMock.Object;
        Console.WriteLine(o1.ReturnTest()); // but this should return TEST
    }

, надеюсь, это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...