AutoFixture: Configuring an Open Generics Specimen Builder
Asked Answered
P

2

10

I have an object model that uses Open Generics (Yes, yes, now I have two problems; that's why I'm here :) :-

public interface IOGF<T>
{
}

class C
{
}

class D
{
    readonly IOGF<C> _ogf;

    public D( IOGF<C> ogf )
    {
        _ogf = ogf;
    }
} 

I'm trying to get AutoFixture to generate Anonymous instances of D above. However, on its own, AutoFixture doesn't have a built in strategy for building an IOGF<> and hence we observe:

public class OpenGenericsBinderDemo
{
    [Fact]
    public void X()
    {
        var fixture = new Fixture();

        Assert.Throws<Ploeh.AutoFixture.ObjectCreationException>( () =>
            fixture.CreateAnonymous<D>() );
    }

The underlying message is:

Ploeh.AutoFixture.ObjectCreationException : AutoFixture was unable to create an instance from IOGF`1[C], most likely because it has no public constructor, is an abstract or non-public type.

I'm happy to provide it a concrete implementation:

public class OGF<T> : IOGF<T>
{
    public OGF( IX x )
    {
    }
}

public interface IX
{
}

public class X : IX
{
}

And an associated binding:

fixture.Register<IX,X>();

How do I (or should I even look at the problem that way??) make the following test pass?

public class OpenGenericsLearning
{
    [Fact]
    public void OpenGenericsDontGetResolved()
    {
        var fixture = new Fixture();
        fixture.Inject<IX>( fixture.Freeze<X>() );

        // TODO register or do something that will provide 
        //      OGF<C> to fulfill D's IOGF<C> requirement

        Assert.NotNull( fixture.CreateAnonymous<D>());
    }
}

(There are discussions and issues around this on the codeplex site - I just needed to a quick impl of this and am open to deleting this if this is just a bad idea and/or I've missed something)

EDIT 2: (See also comment on Mark's answer) The (admittedly contrived) context here is an acceptance test on a large 'almost full system' System Under Test object graph rather than a small (controlled/easy to grok :) pair or triplet of classes in a unit or integration test scenario. As alluded to in the self-question parenthetical statement, I'm not fully confident this type of test even makes sense though.

Peru answered 10/4, 2012 at 16:14 Comment(7)
AutoMoq, AutoRhinoMocks, and AutoFakeItEasy are extensions that allow you to use AutoFixture as an auto-mocking container. Is this an option? (Because that way you can successfully create an anonymous instance of D.)Intellectual
@Nikos Am aware of the automocking extensions and had mentally ruled them out - shoud have mentioned that; see my comments on Mark's answer for some more background). Taking this comment together with Mark's answer to heart, I'll be redoubling my efforts to avoid relying on my crutch!Peru
related: #20209969Peru
@RubenBartelink Curious, what was this open generic code used for? I love when people use open generics to solve problems, but the C# irregular type system for generics, with corner cases and gotchas, sometimes makes me feel the effort is not worth it.Feola
@JohnZabroski Ha, I can't remember now - testing stuff using some generic repository antipattern?!Peru
Why are generic repositories antipatterns? I feel like that's OO programmer speak for "I like to write lots of code and want job security and not add value to my company's top line by delaying project timelines."Feola
Was just trying to convey that this didnt particularly feel right to me at the time. I don't recall this being a common need for me (I mainly write F# these days, and using such a construct is not something I recall arriving at since)Peru
P
9

You could create a customization which works as follows:

public class AnOpenGenericsBinderDemo
{
    [Fact]
    public void RegisteringAGenericBinderShouldEnableResolution()
    {
        var fixture = new Fixture();
        fixture.Inject<IX>( fixture.Freeze<X>() );
        fixture.RegisterOpenGenericImplementation( typeof( IOGF<> ), typeof( OGF<> ) );

        Assert.IsType<OGF<C>>( fixture.CreateAnonymous<D>().Ogf );
    }
}

And is implemented like so:

public static class AutoFixtureOpenGenericsExtensions
{
    public static void RegisterOpenGenericImplementation( this IFixture that, Type serviceType, Type componentType )
    {
        if ( !serviceType.ContainsGenericParameters )
            throw new ArgumentException( "must be open generic", "serviceType" );
        if ( !componentType.ContainsGenericParameters )
            throw new ArgumentException( "must be open generic", "componentType" );
        // TODO verify number of type parameters is 1 in each case
        that.Customize( new OpenGenericsBinderCustomization( serviceType, componentType ) );
    }

    public class OpenGenericsBinderCustomization : ICustomization
    {
        readonly Type _serviceType;
        readonly Type _componentType;

        public OpenGenericsBinderCustomization( Type serviceType, Type componentType )
        {
            _serviceType = serviceType;
            _componentType = componentType;
        }

        void ICustomization.Customize( IFixture fixture )
        {
            fixture.Customizations.Add( new OpenGenericsSpecimenBuilder( _serviceType, _componentType ) );
        }

        class OpenGenericsSpecimenBuilder : ISpecimenBuilder
        {
            readonly Type _serviceType;
            readonly Type _componentType;

            public OpenGenericsSpecimenBuilder( Type serviceType, Type componentType )
            {
                _serviceType = serviceType;
                _componentType = componentType;
            }

            object ISpecimenBuilder.Create( object request, ISpecimenContext context )
            {
                var typedRequest = request as Type;
                if ( typedRequest != null && typedRequest.IsGenericType && typedRequest.GetGenericTypeDefinition() == _serviceType )
                    return context.Resolve( _componentType.MakeGenericType( typedRequest.GetGenericArguments().Single() ) );
                return new NoSpecimen( request );
            }
        }
    }
}

I assume someone has a better implementation than that though and/or there is a built-in implementation.

EDIT: The following is the updated D with the sensing property:

class D
{
    readonly IOGF<C> _ogf;

    public D( IOGF<C> ogf )
    {
        _ogf = ogf;
    }

    public IOGF<C> Ogf
    {
        get { return _ogf; }
    }
}
Peru answered 10/4, 2012 at 16:17 Comment(3)
I got an exception: System.ArgumentException: Object of type 'Ploeh.AutoFixture.Kernel.OmitSpecimen' cannot be converted to type 'SelogerCity.Data.Entities.User'. with the above customization. Type would have been ISet<User> or IList<User>... way to much work to make this library useful.Exterritorial
@Exterritorial sorry, didnt see this till now. Have you got a failing test? (I ended up using this code and it has to date met my needs but I don't know what you're doing and hence can't really begin to guess which cast or comparison in my code needs improvement)Peru
Fantastic, works as expected for IObservable<> -> Subject<> in the Rx libraryPlyler
N
4

AFICT there are no open generics in sight. D relies on IOGF<C> which is a constructed type.

The error message isn't because of open generics, but because IOGF<C> is an interface.

You can supply a mapping from IOGF<C> to OGF<C> like this:

fixture.Register<IOGF<C>>(() => fixture.CreateAnonymous<OGF<C>>());

Since OGF<C> relies on IX you'll also need to supply a mapping to X:

fixture.Register<IX>(() => fixture.CreateAnonymous<X>());

That should do the trick.

However, as Nikos Baxevanis points out in his comment, if you use one of the three supplied auto-mocking extensions, this would basically work out of the box - e.g.

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var d = fixture.CreateAnonymous<D>();
Nonna answered 10/4, 2012 at 18:45 Comment(2)
+1 Thanks Mark. I guess the question is a bit contrived, and I didnt put enough energy into making it credible. What I was driving at was a desire to be able to match the open generic binding capabilities of a DI framework (and conventions extension). This originates from the fact that I/we are [ab]using AF a lot in acceptance and integration tests, wherein the ability to construct all sorts of convoluted context classes and helpers shines (but also is a slipperly slope to over-complex tests). Your point is well made that there should be no need for such a binding in any sensible unit tests.Peru
(I'm aware of most of the stuff you've cited and agree with the design decision to not gratuitously offer overloads (e.g. of Register) that take a `Type, which could potentially make this neater. The chances are that the actual problem will be resolved by redesigning the test context classes/fixtures for which this desire exists.Peru

© 2022 - 2024 — McMap. All rights reserved.