具有"弱"类型的AutoFixture

nik*_*d23 5 xunit autofixture

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

这是我的场景,使用IInterceptor来自Castle Dynamic Proxy的实现进行说明.

首先是被测系统:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

现在进行一些简单的测试,利用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
    }
}
Run Code Online (Sandbox Code Playgroud)

我的CustomAutoData属性,事实上确实定制AutoFixture,这样的注射情况IInvocation大多正确配置,但每IInterceptor实现预期完全不同类型的ProxyReturnValue性质,每个测试必须设置这些自己.(因此Mock.Get(context).Setup(...)电话.)

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

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

Mar*_*ann 7

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

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

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

首先,你不具备为代理服务器和返回值的属性设置返回值,如果你不需要它们.

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
}
Run Code Online (Sandbox Code Playgroud)

直接指定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);
}
Run Code Online (Sandbox Code Playgroud)

但是,它只适用于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;
                });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意来电SetupAllProperties.

这是因为AutoFixture.AutoMoq通过将所有接口请求中继到对该接口的模拟请求 - 即请求IInvocation转换为请求来工作Mock<IInvocation>.

不要设置测试值; 读回来

最后,您应该问自己:我是否真的需要为这些属性分配特定值(例如"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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

(如果要求值始终为字符串,则可以将构造函数签名更改为需要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);
}
Run Code Online (Sandbox Code Playgroud)

这种方法的好处是,在这些测试情况下,你不关心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;
                });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,通过要求IFixture实例创建相应信号类型的新实例然后展开其值来分配每个属性的值.

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