Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AutoFixture with "weak" types

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?

like image 532
nikmd23 Avatar asked Dec 19 '12 20:12

nikmd23


1 Answers

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.

like image 84
Mark Seemann Avatar answered Nov 04 '22 01:11

Mark Seemann