带有“弱"的 AutoFixture类型 [英] AutoFixture with "weak" types

查看:13
本文介绍了带有“弱"的 AutoFixture类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I love AutoFixture, but have run into a bit of very repetitive "arrange" code that I feel like it should be able to handle - somehow.

这是我的场景,使用来自 Castle 动态代理的 IInterceptor 的实现进行说明.

Here is my scenario, illustrated using implementations of IInterceptor from 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
    }
}

现在进行一些利用 xUnit 的数据理论支持的简单测试:

Now for a few simple tests which leverage the data theories support for 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(...) 调用.)

My CustomAutoData attribute does in fact customize AutoFixture so that the injected instances of IInvocation are mostly configured properly, but since every IInterceptor implementation expects completely different types for the Proxy and ReturnValue properties, each test has to set those on their own. (Thus the Mock.Get(context).Setup(...) calls.)

这没问题,除了 InterceptorATests 中的每个测试必须重复相同的几行排列,以及 InterceptorBTests 中的每个测试.

This is okay, except that every test in InterceptorATests must repeat the same few lines of arrangement, as well as every test in InterceptorBTests.

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

Is there a way to cleanly remove the repetitive Mock.Get(...) calls? Is there a good way to access the IFixture instance for a given test class?

推荐答案

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

There are tons of things you can do - depending on exactly what it is that you really want to test.

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

First of all I would like to point out that much of the trouble in this particular question originates in the extremely weakly typed API of IInvocation, as well as the fact that Moq doesn't implement properties as we normally implement properties.

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

首先,如果您不需要它们,您不必为 Proxy 和 ReturnValue 属性设置返回值.

First of all, you don't have to setup return values for the Proxy and ReturnValue properties if you don't need them.

AutoFixture.AutoMoq 设置Mock 实例的方式是它始终设置DefaultValue = DefaultValue.Mock.由于两个属性的返回类型都是 object 并且 object 有一个默认的构造函数,所以你会自动得到一个对象(实际上是一个 ObjectProxy).

The way AutoFixture.AutoMoq sets up Mock<T> instances is that it always sets DefaultValue = DefaultValue.Mock. Since the return type of both properties is object and object has a default constructor, you will automatically get an object (actually, an ObjectProxy) back.

换句话说,这些测试也通过了:

In other words, these tests also pass:

[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

对于我的其余答案,我将假设您实际上需要在测试中分配和/或读取属性值.

For the rest of my answer, I'm going to assume that you actually need to assign and/or read the property values in your tests.

首先,您可以通过直接分配 ReturnValue 来减少繁重的 Moq 语法:

First of all, you can cut down on the heavy Moq syntax by assigning the ReturnValue directly:

[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 属性,因为它是只读的(不会编译).

However, it only works for ReturnValue since it's a writable property. It doesn't work with the Proxy property because it's read-only (it's not going to compile).

为了完成这项工作,您必须指示 Moq 将 IInvocation 属性视为真实"属性:

In order to make this work, you must instruct Moq to treat IInvocation properties as 'real' properties:

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 的请求;.

This works because AutoFixture.AutoMoq works by relaying all requests for interfaces to a request for a Mock of that interface - i.e. a request for IInvocation is converted to a request for Mock<IInvocation>.

不要设置测试值;再读一遍

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

In the end, you should ask yourself: Do I really need to assign specific values (such as "a", "b" and "z") to these properties. Couldn't I just let AutoFixture create the required values? And if I do that, do I need to explicitly assign them? Couldn't I just read back the assigned value instead?

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

This is possibly with a little trick I call Signal Types. A Signal Type is a class that signals a particular role of a value.

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

Introduce a signal type for each property:

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.)

(If you require the values to always be strings, you can change the constructor signature to require a string instead of an object.)

冻结您关心的信号类型,以便您知道在配置 IInvocation 实例时将重用相同的实例:

Freeze the Signal Types you care about so that you know the same instance is going to be reused when the IInvocation instance is configured:

[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 的测试用例中,您可以省略这些方法参数.

The beauty of this approach is that in those test cases where you don't care about the ReturnValue or Proxy you can just omit those method arguments.

对应的Customization是之前的扩展:

The corresponding Customization is an expansion of the previous:

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

Notice the that value for each property is assigned by asking the IFixture instance to create a new instance of the corresponding Signal Type and then unwrapping its value.

这种方法可以推广,但这就是它的要点.

This approach can be generalized, but that's the gist of it.

这篇关于带有“弱"的 AutoFixture类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆