我喜欢AutoFixture,但遇到了一些非常重复的“排列”代码,我觉得它应该能够处理 - 不知何故。
以下是我的方案,使用来自Castle Dynamic Proxy的IInterceptor
的实现来说明。
首先是被测系统:
public class InterceptorA : IInterceptor
{
public void Intercept(IInvocation context)
{
object proxy = context.Proxy;
object returnValue = context.ReturnValue;
// Do something with proxy and returnValue
}
}
public class InterceptorB : IInterceptor
{
public void Intercept(IInvocation context)
{
object returnValue = context.ReturnValue;
// Do something with different returnValue
}
}
现在进行一些简单的测试,利用xUnit的数据理论支持:
public class InterceptorATests
{
[Theory, CustomAutoData]
public void TestA1(InterceptorA sut, IInvocation context)
{
Mock.Get(context).Setup(c => c.Proxy).Returns("a");
Mock.Get(context).Setup(c => c.ReturnValue).Returns("b");
sut.Intercept(context);
// assert
}
}
public class InterceptorBTests
{
[Theory, CustomAutoData]
public void TestB1(InterceptorB sut, IInvocation context)
{
Mock.Get(context).Setup(c => c.ReturnValue).Returns("z");
sut.Intercept(context);
// assert
}
}
我的CustomAutoData
属性确实自定义了AutoFixture,以便IInvocation
的注入实例主要配置正确,但由于每个IInterceptor
实现都要求完全不同Proxy
和ReturnValue
属性的类型,每个测试都必须自己设置。 (因此Mock.Get(context).Setup(...)
来电。)
这是可以的,除了InterceptorATests
中的每个测试必须重复相同的几行排列,以及InterceptorBTests
中的每个测试。
有没有办法彻底删除重复的Mock.Get(...)
来电?有没有一种方法可以访问给定测试类的IFixture
实例?
答案 0 :(得分:7)
你可以做很多事情 - 完全取决于你真正想要测试的 。
首先,我想指出的是,这个特定问题的大部分问题都源于IInvocation极其弱类型的API,以及Moq没有像我们通常实现属性那样实现属性的事实
如果您不需要,请不要设置存根
首先,如果您不需要,则不要 设置Proxy和ReturnValue属性的返回值。
AutoFixture.AutoMoq设置Mock<T>
个实例的方式是它始终设置DefaultValue = DefaultValue.Mock
。由于两个属性的返回类型为object
而object
具有默认构造函数,因此您将自动返回一个对象(实际上是ObjectProxy
)。
换句话说,这些测试也通过了:
[Theory, CustomAutoData]
public void TestA2(InterceptorA sut, IInvocation context)
{
sut.Intercept(context);
// assert
}
[Theory, CustomAutoData]
public void TestB2(InterceptorB sut, IInvocation context)
{
sut.Intercept(context);
// assert
}
直接指定ReturnValue
对于我的其余部分,我将假设您确实需要在测试中分配和/或读取属性值。
首先,您可以通过直接指定ReturnValue来减少沉重的Moq语法:
[Theory, Custom3AutoData]
public void TestA3(InterceptorA sut, IInvocation context)
{
context.ReturnValue = "b";
sut.Intercept(context);
// assert
Assert.Equal("b", context.ReturnValue);
}
[Theory, Custom3AutoData]
public void TestB3(InterceptorB sut, IInvocation context)
{
context.ReturnValue = "z";
sut.Intercept(context);
// assert
Assert.Equal("z", context.ReturnValue);
}
但是,它仅适用于ReturnValue
,因为它是可写属性。它不适用于Proxy
属性,因为它是只读的(它不会编译)。
为了完成这项工作,您必须指示Moq将IInvocation
属性视为真实的&#39;属性:
public class Customization3 : CompositeCustomization
{
public Customization3()
: base(
new RealPropertiesOnInvocation(),
new AutoMoqCustomization())
{
}
private class RealPropertiesOnInvocation : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Register<Mock<IInvocation>>(() =>
{
var td = new Mock<IInvocation>();
td.DefaultValue = DefaultValue.Mock;
td.SetupAllProperties();
return td;
});
}
}
}
请注意对SetupAllProperties
的调用。
这是有效的,因为AutoFixture.AutoMoq的工作原理是将所有接口请求转发给该接口Mock的请求 - 即IInvocation
的请求转换为Mock<IInvocation>
的请求。
不要设置测试值;读回来
最后,您应该问自己:我是否真的需要分配特定的值(例如&#34; a&#34;,&#34; b&#34;和&#34; z&#34;)这些属性。我不能让AutoFixture创建所需的值吗?如果我这样做,我是否需要明确指定它们?我不能回读指定的值吗?
这可能是我称之为信号类型的小技巧。信号类型是指示值的特定角色的类。
为每个属性引入信号类型:
public class InvocationReturnValue
{
private readonly object value;
public InvocationReturnValue(object value)
{
this.value = value;
}
public object Value
{
get { return this.value; }
}
}
public class InvocationProxy
{
private readonly object value;
public InvocationProxy(object value)
{
this.value = value;
}
public object Value
{
get { return this.value; }
}
}
(如果您要求值始终为字符串,则可以将构造函数签名更改为需要string
而不是object
。)
冻结您关心的信号类型,以便在配置IInvocation实例时知道将重用相同的实例:
[Theory, Custom4AutoData]
public void TestA4(
InterceptorA sut,
[Frozen]InvocationProxy proxy,
[Frozen]InvocationReturnValue returnValue,
IInvocation context)
{
sut.Intercept(context);
// assert
Assert.Equal(proxy.Value, context.Proxy);
Assert.Equal(returnValue.Value, context.ReturnValue);
}
[Theory, Custom4AutoData]
public void TestB4(
InterceptorB sut,
[Frozen]InvocationReturnValue returnValue,
IInvocation context)
{
sut.Intercept(context);
// assert
Assert.Equal(returnValue.Value, context.ReturnValue);
}
这种方法的优点在于,在您不关心ReturnValue
或Proxy
的测试用例中,您可以省略这些方法参数。
相应的Customization是前一个的扩展:
public class Customization4 : CompositeCustomization
{
public Customization4()
: base(
new RelayedPropertiesOnInvocation(),
new AutoMoqCustomization())
{
}
private class RelayedPropertiesOnInvocation : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Register<Mock<IInvocation>>(() =>
{
var td = new Mock<IInvocation>();
td.DefaultValue = DefaultValue.Mock;
td.SetupAllProperties();
td.Object.ReturnValue =
fixture.CreateAnonymous<InvocationReturnValue>().Value;
td.Setup(i => i.Proxy).Returns(
fixture.CreateAnonymous<InvocationProxy>().Value);
return td;
});
}
}
}
请注意,通过要求IFixture实例创建相应信号类型的新实例然后展开其值来分配每个属性的值。
这种方法可以概括,但这是它的要点。
答案 1 :(得分:0)
我最终在xUnit的扩展点上降低了一个级别来解决这个问题 - 受到Mark答案中提到的信号类型模式的启发。
现在我的测试有一个额外的属性:Signal
。
public class InterceptorATests
{
[Theory, CustomAutoData]
public void TestA1(InterceptorA sut, [Signal(typeof(SpecialContext))] IInvocation context)
{
// no more repetitive arrangement!
sut.Intercept(context);
// assert
}
}
SignalAttribute
类非常简单:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class SignalAttribute : Attribute
{
public ISignalType SignalType { get; set; }
public SignalAttribute(Type customization)
{
SignalType = (ISignalType)Activator.CreateInstance(customization);
}
}
真正的魔力出现在我新近更新的CustomAutoData
课程中:
public class CustomAutoDataAttribute: AutoDataAttribute
{
public CustomAutoDataAttribute() : base(new Fixture().Customize(new AutoMoqCustomization()))
{
}
public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
{
Type input = null;
ISignalType signalType = null;
foreach (var parameter in methodUnderTest.GetParameters())
{
var attribute = parameter.GetCustomAttribute(typeof(SignalAttribute)) as SignalAttribute;
if (attribute == null)
continue;
input = parameter.ParameterType;
signalType = attribute.SignalType;
break;
// this proof of concept only supports one parameter at a time
}
var result = base.GetData(methodUnderTest, parameterTypes);
if (input == null)
return result;
int index = Array.IndexOf(parameterTypes, input);
foreach (var objectSet in result)
{
signalType.Customize(objectSet[index]);
}
return result;
}
}
最后,我只是创建了我的SpecialContext
。我在InterceptorATests
中将其创建为嵌套类,但它可以存在于任何地方:
public class SpecialContext : ISignalType
{
public void Customize(object obj)
{
var input = (IInvocation)obj;
Mock.Get(input).Setup(i => i.Proxy).Returns("a");
Mock.Get(input).Setup(i => i.ReturnValue).Returns("b");
}
}
这使我能够在AutoFixture完成大多数创建IInvocation
的工作后有效地挂钩,但在一个地方指定进一步的自定义。
注意:这是概念代码的证明!它没有正确处理许多场景。使用风险自负。