Search code examples
abstract-classautofixture

Can AutoFixture omit recursion between class hierarchies?


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'.

Solution

  • 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 ISpecimenBuilders 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.