Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use custom ISpecimenBuilders with OmitOnRecursionBehavior?

Tags:

autofixture

How can I use custom ISpecimenBuilder instances along with the OmitOnRecursionBehavior which I want applied globally to all fixture-created objects?

I'm working with an EF Code First model with a foul-smelling circular reference that, for the purposes of this question, cannot be eliminated:

public class Parent {
    public string Name { get; set; }
    public int Age { get; set; }
    public virtual Child Child { get; set; }
}

public class Child {
    public string Name { get; set; }
    public int Age { get; set; }
    public virtual Parent Parent { get; set; }
}

I'm familiar with the technique for side-stepping circular references, as in this passing test:

[Theory, AutoData]
public void CanCreatePatientGraphWithAutoFixtureManually(Fixture fixture)
{
    //fixture.Customizations.Add(new ParentSpecimenBuilder());
    //fixture.Customizations.Add(new ChildSpecimenBuilder());
    fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
                     .ForEach(b => fixture.Behaviors.Remove(b));
    fixture.Behaviors.Add(new OmitOnRecursionBehavior());
    fixture.Behaviors.Add(new TracingBehavior());
    var parent = fixture.Create<Parent>();
    parent.Should().NotBeNull();
    parent.Child.Should().NotBeNull();
    parent.Child.Parent.Should().BeNull();
}

But if either/both customizations are uncommented, I get an exception:

System.InvalidCastException: Unable to cast object of type
'Ploeh.AutoFixture.Kernel.OmitSpecimen' to type 'CircularReference.Parent'.

The failing cast is occurring in my ISpecimenBuilder implementations (shown at the bottom of this question) when I call on the ISpecimenContext to resolve Parent and the request is coming from the Child being resolved. I could guard against the request coming from the Child resolving operation like this:

//...
&& propertyInfo.ReflectedType != typeof(Child)
//...

But, that seems to pollute the ISpecimenBuilder implementation with knowledge of 'who' might be making the request. Also, it seems to duplicate the work that the 'global' OmitOnRecursionBehavior is meant to do.

I want to use the ISpecimenBuilder instances because I have other things to customize besides handling the circular reference. I've spent a lot of time looking for examples of a scenario like this here on SO and also on Ploeh but I haven't found anything yet that discusses the combination of behaviors and customizations. It's important that the solution be one that I can encapsulate with ICustomization, rather than lines and lines in the test setup of

//...
fixture.ActLikeThis(new SpecialBehavior())
       .WhenGiven(typeof (Parent))
       .AndDoNotEvenThinkAboutBuilding(typeof(Child))
       .UnlessParentIsNull()
//...

...because ultimately I want to extend an [AutoData] attribute for tests.

What follows are my ISpecimenBuilder implementations and the output of the TracingBehavior for the failing test:

public class ChildSpecimenBuilder : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        var propertyInfo = request as PropertyInfo;
        return propertyInfo != null
               && propertyInfo.PropertyType == typeof(Child)
                   ? Resolve(context)
                   : new NoSpecimen(request);
    }

    private static object Resolve(ISpecimenContext context)
    {
        var child = (Child) context.Resolve(typeof (Child));
        child.Name = context.Resolve(typeof (string)).ToString().ToLowerInvariant();
        child.Age = Math.Min(17, (int) context.Resolve(typeof (int)));
        return child;
    }
}

public class ParentSpecimenBuilder : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        var propertyInfo = request as PropertyInfo;
        return propertyInfo != null
               && propertyInfo.PropertyType == typeof (Parent)
                   ? Resolve(context)
                   : new NoSpecimen(request);
    }

    private static object Resolve(ISpecimenContext context)
    {
        var parent = (Parent) context.Resolve(typeof (Parent));
        parent.Name = context.Resolve(typeof (string)).ToString().ToUpperInvariant();
        parent.Age = Math.Max(18, (int) context.Resolve(typeof (int)));
        return parent;
    }
}

CanCreatePatientGraphWithAutoFixtureManually(fixture: Ploeh.AutoFixture.Fixture) : Failed  Requested: Ploeh.AutoFixture.Kernel.SeededRequest
    Requested: CircularReference.Parent
      Requested: System.String Name
        Requested: Ploeh.AutoFixture.Kernel.SeededRequest
          Requested: System.String
          Created: 38ab48f4-b071-40f0-b713-ef9d4c825a85
        Created: Name38ab48f4-b071-40f0-b713-ef9d4c825a85
      Created: Name38ab48f4-b071-40f0-b713-ef9d4c825a85
      Requested: Int32 Age
        Requested: Ploeh.AutoFixture.Kernel.SeededRequest
          Requested: System.Int32
          Created: 9
        Created: 9
      Created: 9
      Requested: CircularReference.Child Child
        Requested: Ploeh.AutoFixture.Kernel.SeededRequest
          Requested: CircularReference.Child
            Requested: System.String Name
              Requested: Ploeh.AutoFixture.Kernel.SeededRequest
                Requested: System.String
                Created: 1f5ca160-b211-4f82-871f-11882dbcf00d
              Created: Name1f5ca160-b211-4f82-871f-11882dbcf00d
            Created: Name1f5ca160-b211-4f82-871f-11882dbcf00d
            Requested: Int32 Age
              Requested: Ploeh.AutoFixture.Kernel.SeededRequest
                Requested: System.Int32
                Created: 120
              Created: 120
            Created: 120
            Requested: CircularReference.Parent Parent
              Requested: CircularReference.Parent
              Created: Ploeh.AutoFixture.Kernel.OmitSpecimen

System.InvalidCastException: Unable to cast object of type 'Ploeh.AutoFixture.Kernel.OmitSpecimen' to type 'CircularReference.Parent'.
like image 330
Jeff Avatar asked Sep 03 '13 18:09

Jeff


1 Answers

Is it an option to customize the creation algorithm using the Customize method?

If yes, you can create and use the following [ParentChildConventions] attribute:

internal class ParentChildConventionsAttribute : AutoDataAttribute
{
    internal ParentChildConventionsAttribute()
        : base(new Fixture().Customize(new ParentChildCustomization()))
    {
    }
}

internal class ParentChildCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<Child>(c => c
            .With(x => x.Name,
                fixture.Create<string>().ToLowerInvariant())
            .With(x => x.Age,
                Math.Min(17, fixture.Create<int>()))
            .Without(x => x.Parent));

        fixture.Customize<Parent>(c => c
            .With(x => x.Name,
                fixture.Create<string>().ToUpperInvariant())
            .With(x => x.Age,
                Math.Min(18, fixture.Create<int>())));
    }
}

The original test, using the [ParentChildConventions] attribute, passes:

[Theory, ParentChildConventions]
public void CanCreatePatientGraphWithAutoFixtureManually(
    Parent parent)
{
    parent.Should().NotBeNull();
    parent.Child.Should().NotBeNull();
    parent.Child.Parent.Should().BeNull();
}
like image 176
Nikos Baxevanis Avatar answered Jan 03 '23 23:01

Nikos Baxevanis