4

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'.
Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402

1 Answers1

4

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.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • Thanks a lot for your detailed and swift responses, Mark! They really do make using libraries of yours a very pleasant experience :) This answer helped me understand a lot better why AutoFixture doesn't work the way I expected it to, and allowed me to find a workaround. Eventually, we settled on something equivalent to `fixture.Customize(x => x.Without(c => c.Parent)`, which works in our case because we never ask explicitly for a child first; we always enter the recursive cycle from the `Parent` side. Thanks again! – Tomas Aschan Jan 31 '18 at 07:23