Consider the following way to represent family members:
public abstract class Human { }
public abstract class Child : Human
{
public ICollection<Parent> Parents { get; set; }
}
public abstract class Parent : Human
{
public ICollection<Child> Children { get; set; }
}
public class Son : Child { }
public class Daughter : Child { }
public class Mum : Parent { }
public class Dad : Parent { }
Now, I want AutoFixture
to generate a Parent
, chosen randomly between Mum
and Dad
, and where the children are randomly chosen between Son
and Daughter
. I also want it to omit recursion, so if it's coming from Parent
and generating a Child
, it can omit the link back to Parent
.
I tried the following customization:
var fixture = new Fixture();
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var random = new Random();
fixture.Register<Parent>(() =>
{
switch (random.Next(1, 2))
{
case 1:
return fixture.Create<Mum>();
case 2:
return fixture.Create<Dad>();
default:
throw new NotImplementedException();
}
});
fixture.Register<Child>(() =>
{
switch (random.Next(1, 2))
{
case 1:
return fixture.Create<Son>();
case 2:
return fixture.Create<Daughter>();
default:
throw new NotImplementedException();
}
});
fixture.Create<Parent>();
but it throws an InvalidCastException
(see below).
Is there a way to configure AutoFixture so that it considers Parent -> Child -> Parent
recursion, even though it actually randomly selects an appropriate subclass for each instance?
Unhandled Exception: AutoFixture.ObjectCreationExceptionWithPath: AutoFixture was unable to
create an instance from AutoFixtureAbstractTrees.Parent because creation unexpectedly
failed with exception. Please refer to the inner exception to investigate the root cause of
the failure.
Request path:
AutoFixtureAbstractTrees.Mum
System.Collections.Generic.ICollection`1[AutoFixtureAbstractTrees.Child] Children
System.Collections.Generic.ICollection`1[AutoFixtureAbstractTrees.Child]
System.Collections.Generic.List`1[AutoFixtureAbstractTrees.Child]
System.Collections.Generic.IEnumerable`1[AutoFixtureAbstractTrees.Child] collection
System.Collections.Generic.IEnumerable`1[AutoFixtureAbstractTrees.Child]
AutoFixtureAbstractTrees.Child
AutoFixtureAbstractTrees.Son
System.Collections.Generic.ICollection`1[AutoFixtureAbstractTrees.Parent] Parents
System.Collections.Generic.ICollection`1[AutoFixtureAbstractTrees.Parent]
System.Collections.Generic.List`1[AutoFixtureAbstractTrees.Parent]
System.Collections.Generic.IEnumerable`1[AutoFixtureAbstractTrees.Parent] collection
System.Collections.Generic.IEnumerable`1[AutoFixtureAbstractTrees.Parent]
AutoFixtureAbstractTrees.Parent
Inner exception messages:
System.InvalidCastException: Unable to cast object of type
'AutoFixture.Kernel.OmitSpecimen' to type 'AutoFixtureAbstractTrees.Mum'.
The reason you're experiencing this problem is because of a design flaw in AutoFixture. When you use the Create
extension method, you essentially kick off a new resolution context, and the recursion guard mechanism doesn't catch that.
It looks like, in this case, you can work around the problem by using ISpecimenBuilder
s and Resolve
from context
instead of using the Create
extension method:
[Fact]
public void WorkAround()
{
var fixture = new Fixture();
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior(3));
var random = new Random();
fixture.Customizations.Add(new ParentBuilder(random));
fixture.Customizations.Add(new ChildBuilder(random));
var actual = fixture.Create<Parent>();
Assert.True(0 < actual.Children.Count);
}
This test passes, and uses the custom classes ParentBuilder
and ChildBuilder
:
public class ParentBuilder : ISpecimenBuilder
{
private readonly Random random;
public ParentBuilder(Random random)
{
this.random = random;
}
public object Create(object request, ISpecimenContext context)
{
var t = request as Type;
if (t == null || t != typeof(Parent))
return new NoSpecimen();
if (this.random.Next(0, 2) == 0)
return context.Resolve(typeof(Mum));
else
return context.Resolve(typeof(Dad));
}
}
public class ChildBuilder : ISpecimenBuilder
{
private readonly Random random;
public ChildBuilder(Random random)
{
this.random = random;
}
public object Create(object request, ISpecimenContext context)
{
var t = request as Type;
if (t == null || t != typeof(Child))
return new NoSpecimen();
if (this.random.Next(0, 2) == 0)
return context.Resolve(typeof(Son));
else
return context.Resolve(typeof(Daughter));
}
}
All that said, as you've previously discovered, you're pushing the limits of AutoFixture here. It's not really designed to deal with complex recursive object designs like the one shown here.