具有“弱”类型的AutoFixture

时间:2012-12-19 20:33:14

标签: xunit autofixture

我喜欢AutoFixture,但遇到了一些非常重复的“排列”代码,我觉得它应该能够处理 - 不知何故。

以下是我的方案,使用来自Castle Dynamic ProxyIInterceptor的实现来说明。

首先是被测系统:

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实现都要求完全不同ProxyReturnValue属性的类型,每个测试都必须自己设置。 (因此Mock.Get(context).Setup(...)来电。)

这是可以的,除了InterceptorATests中的每个测试必须重复相同的几行排列,以及InterceptorBTests中的每个测试。

有没有办法彻底删除重复的Mock.Get(...)来电?有没有一种方法可以访问给定测试类的IFixture实例?

2 个答案:

答案 0 :(得分:7)

你可以做很多事情 - 完全取决于你真正想要测试的

首先,我想指出的是,这个特定问题的大部分问题都源于IInvocation极其弱类型的API,以及Moq没有像我们通常实现属性那样实现属性的事实

如果您不需要,请不要设置存根

首先,如果您不需要,则不要 设置Proxy和ReturnValue属性的返回值。

AutoFixture.AutoMoq设置Mock<T>个实例的方式是它始终设置DefaultValue = DefaultValue.Mock。由于两个属性的返回类型为objectobject具有默认构造函数,因此您将自动返回一个对象(实际上是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);
}

这种方法的优点在于,在您不关心ReturnValueProxy的测试用例中,您可以省略这些方法参数。

相应的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的工作后有效地挂钩,但在一个地方指定进一步的自定义。

注意:这是概念代码的证明!它没有正确处理许多场景。使用风险自负。