围绕现有实例使用最小起订量创建模拟?
Creating Mock with Moq around existing instance?
假设我们有大量配置的集成测试 IConfiguration
。我已经将测试设置为与 autofac 容器一起使用,现在我想使用 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
但我希望有一种方法可以调用基值而不是仅仅调用基实现。
我相信默认情况下 Moq
允许您为 IConfiguration
实现传递构造函数参数,它会为您创建一个 class 的新实例。如果我正确理解您的问题,您宁愿使用预构建的实例。
我假设您知道 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 and it actually allows us to generate proxies for instances (they call it Class proxy with target)。因此问题是 - 我们如何让 Moq
为我们制作一个。
似乎没有开箱即用的选项,并且简单地注入覆盖并不太顺利,因为库内没有太多的控制反转,并且大多数类型和属性都标记为 internal
,继承几乎不可能。
然而,Castle Proxy
更受用户欢迎,并且公开了相当多的方法并可用于覆盖。因此,让我们定义一个 ProxyGenerator
class ,它将接受方法 Moq
调用并向其添加所需的功能(只需比较 CreateClassProxyWithTarget
and 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
}
希望这对您有所帮助。
假设我们有大量配置的集成测试 IConfiguration
。我已经将测试设置为与 autofac 容器一起使用,现在我想使用 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
但我希望有一种方法可以调用基值而不是仅仅调用基实现。
我相信默认情况下 Moq
允许您为 IConfiguration
实现传递构造函数参数,它会为您创建一个 class 的新实例。如果我正确理解您的问题,您宁愿使用预构建的实例。
我假设您知道 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 and it actually allows us to generate proxies for instances (they call it Class proxy with target)。因此问题是 - 我们如何让 Moq
为我们制作一个。
似乎没有开箱即用的选项,并且简单地注入覆盖并不太顺利,因为库内没有太多的控制反转,并且大多数类型和属性都标记为 internal
,继承几乎不可能。
Castle Proxy
更受用户欢迎,并且公开了相当多的方法并可用于覆盖。因此,让我们定义一个 ProxyGenerator
class ,它将接受方法 Moq
调用并向其添加所需的功能(只需比较 CreateClassProxyWithTarget
and 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
}
希望这对您有所帮助。