AutoFixture with “weak” types

南楼画角 提交于 2019-12-10 15:11:32

问题


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.

Here is my scenario, illustrated using implementations of IInterceptor from Castle Dynamic Proxy.

First the systems under test:

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

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

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

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

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?


回答1:


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

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.

Don't setup stubs if you don't need them

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

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
}

Directly assign 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.

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

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

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

Notice the call to SetupAllProperties.

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

Don't set the test values; read them back

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

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

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

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.

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

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.




回答2:


I've ended up dropping down a level to xUnit's extensibility points to solve this problem - inspired by the Signal Type pattern mentioned in Mark's answer.

Now my test's have an additional attribute: Signal.

public class InterceptorATests
{
    [Theory, CustomAutoData]
    public void TestA1(InterceptorA sut, [Signal(typeof(SpecialContext))] IInvocation context)
    {
        // no more repetitive arrangement!
        sut.Intercept(context);
        // assert
    }
}

The SignalAttribute class is very simple:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class SignalAttribute : Attribute
{
    public ISignalType SignalType { get; set; }

    public SignalAttribute(Type customization)
    {
        SignalType = (ISignalType)Activator.CreateInstance(customization);
    }
}

The real magic comes in my newly updated CustomAutoData class:

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

Finally, I just create my SpecialContext. I create it as a nested class in InterceptorATests, but it could live anywhere:

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

This allows me to effectively hook in after AutoFixture has done most of the work creating an IInvocation, but specify further customizations in one place.

NOTE: This is proof of concept code! It does not handle many scenarios properly. Use at your own risk.



来源:https://stackoverflow.com/questions/13960681/autofixture-with-weak-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!