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'.
ICustomization
implementation would simply add one or moreISpecimenBuilder
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