How can I use custom ISpecimenBuilders with OmitOnRecursionBehavior?
Asked Answered
U

2

6

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'.
Undistinguished answered 3/9, 2013 at 18:3 Comment(0)
S
5

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();
}
Scanlan answered 3/9, 2013 at 21:12 Comment(3)
Great solution. I think I was trying to make it too complicated, but I had seen various solutions in which the ICustomization implementation would simply add one or more ISpecimenBuilder implementations to the fixture's Customizations collection. Am I missing the point about when it's more appropriate to do that vs. your solution?Undistinguished
Usually you create an ISpecimenBuilder for infrastructure purposes (e.g. to omit properties of a certain kind). To supply specific value(s) to particular member(s) you may start with the Customize method and optionally create an ISpecimenBuilder along the way.Scanlan
Another thing… I notice you've omitted any calls to remove ThrowingRecursionBehavior and/or add OmitOnRecursionBehavior, and yet the tests still pass. Does the .Without() call avoid the detection of this circular reference before ThrowingRecursionBehavior gets a chance to throw an exception?Undistinguished
C
2

You can also use AutoFixture.AutoEntityFramework to help with EF.

Chandigarh answered 11/12, 2015 at 14:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.