【问题标题】:AutoFixture with "weak" types具有“弱”类型的 AutoFixture
【发布时间】:2012-12-19 20:33:13
【问题描述】:

我喜欢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 实现都期望Proxy 的类型完全不同和ReturnValue 属性,每个测试都必须自己设置。 (因此Mock.Get(context).Setup(...) 调用。)

这没关系,只是InterceptorATests 中的每个测试都必须重复相同的几行排列,InterceptorBTests 中的每个测试也是如此。

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

【问题讨论】:

    标签: xunit autofixture


    【解决方案1】:

    您可以做很多事情 - 取决于您真正想要测试的是什么

    首先,我想指出,这个特定问题中的大部分问题都源于 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&lt;IInvocation&gt; 的请求。

    不要设置测试值;回读它们

    最后,您应该问自己:我真的需要为这些属性分配特定的值(例如“a”、“b”和“z”)吗?我不能让 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 实例创建相应信号类型的新实例然后解包其值来分配的。

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

    【讨论】:

    • 感谢马克的精彩回答。我不得不承认这不是我所希望的,而且我过去曾使用过信号类型,但它提供的信息非常丰富,这是我现在必须放弃的。跟上 AutoFixture 的伟大工作!
    • 你希望得到什么?我查看了您提出的解决方案,虽然我意识到这是一个 PoC,但我不明白它如何使任何事情变得更好。
    【解决方案2】:

    我最终降低了 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 的工作后有效地挂接,但在一个地方指定进一步的自定义。

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

    【讨论】:

    • 您是否考虑过从 Ploeh.AutoFixture.Xunit.CustomizeAttribute 派生自定义的参数绑定属性?那不是更容易吗?
    • [Customize] 属性是 AutoFixture.Xunit 属性的基类,例如 [Frozen][FavorArrays] 等。
    • 不,我没有,我不知道该类的存在。我去看看看看!
    • 您的目标是将IInvocation 的设置移动到一个单独的类吗?如果是这样,为什么不使用专门的 [AutoData]ICustomization 对?
    • 我希望将IInvocation 的设置移动到一个方法中,并且可能在将来。
    猜你喜欢
    • 1970-01-01
    • 2012-10-08
    • 2020-02-19
    • 1970-01-01
    • 2015-03-31
    • 2015-12-03
    • 1970-01-01
    • 2016-06-15
    • 2012-11-04
    相关资源
    最近更新 更多