5

我喜欢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给定测试类的实例?

4

2 回答 2

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属性视为“真实”属性:

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>

不要设置测试值;读回来

最后,您应该问自己:我真的需要为这些属性分配特定的值(例如“a”、“b”和“z”)吗?我不能让 AutoFixture 创建所需的值吗?如果我这样做,我是否需要明确分配它们?我不能只读回分配的值吗?

这可能是我称之为Signal Types的一个小技巧。信号类型是一个表示值的特定角色的类。

为每个属性引入一个信号类型:

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; }
    }
}

(如果您要求值始终是字符串,您可以将构造函数签名更改为需要 astring而不是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你可以忽略那些方法参数。

对应的定制是对前面的扩展:

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 实例创建相应信号类型的新实例然后解包其值来分配的。

这种方法可以概括,但这就是它的要点。

于 2012-12-19T22:46:38.107 回答
0

我最终降低了 xUnit 的可扩展性点来解决这个问题 - 灵感来自 Mark 的回答中提到的 Signal Type 模式。

现在我的测试有一个附加属性: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,但在一个地方指定进一步的自定义。

注意:这是概念验证代码!它不能正确处理许多场景。使用风险自负。

于 2012-12-21T19:37:54.610 回答