Controlling the depth of generation of an object tree with Autofixture
Asked Answered
H

3

12

I'm trying to control the depth of generation of an object tree with Autofixture. In some cases I want just to generate the root object and in another set of cases I may want to generate the tree up to a certain depth (2, 3, let's say).

class Foo {
    public string Name {get;set;}
    public Bar Bar {get;set;}
    public AnotherType Xpto {get;set;}
    public YetAnotherType Xpto {get;set;}
}

class Bar {
    public string Name {get;set;}
    public string Description {get;set;}
    public AnotherType Xpto {get;set;}
    public YetAnotherType Xpto {get;set;}
    public Xpto Xpto {get;set;}
}

class Xpto {
    public string Description {get;set;}
    public AnotherType Xpto {get;set;}
    public YetAnotherType Xpto {get;set;}
}

With the example above I would want (depth 1) to control the generation process so that only the Foo class is instantiated and the Bar property or any other reference type on that class is not populated or (depth 2) I would want the Foo class instantiated, the Bar property populated with a new instance of Bar but the Xpto property or any other reference type on that class not populated.

In case I did not spot it in the codebase does Autofixture have a customisation or behaviour to allow us to have that kind of control?

Again, it's not recursion that I want to control but the depth of population of the object graph.

Hobnob answered 13/11, 2013 at 10:25 Comment(4)
can you share your code ?Masthead
possible duplicate of Creating recursive tree with AutoFixtureCampfire
Sorry, Mark, but it's not since the object tree is made of different object types. Its not the recursion I need to control but the depth of population of an object's tree.Hobnob
OK, sorry. I've retracted my close vote and added an answer.Campfire
E
8

You can use the below GenerationDepthBehavior class as follows:

fixture.Behaviors.Add(new GenerationDepthBehavior(2));

public class GenerationDepthBehavior : ISpecimenBuilderTransformation
{
    private const int DefaultGenerationDepth = 1;
    private readonly int generationDepth;

    public GenerationDepthBehavior() : this(DefaultGenerationDepth)
    {
    }

    public GenerationDepthBehavior(int generationDepth)
    {
        if (generationDepth < 1)
            throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");

        this.generationDepth = generationDepth;
    }

    public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
    {
        if (builder == null) throw new ArgumentNullException(nameof(builder));

        return new GenerationDepthGuard(builder, new GenerationDepthHandler(), this.generationDepth);
    }
}

public interface IGenerationDepthHandler
{
    object HandleGenerationDepthLimitRequest(object request, IEnumerable<object> recordedRequests, int depth);
}

public class DepthSeededRequest : SeededRequest
{
    public int Depth { get; }

    public int MaxDepth { get; set; }

    public bool ContinueSeed { get; }

    public int GenerationLevel { get; private set; }

    public DepthSeededRequest(object request, object seed, int depth) : base(request, seed)
    {
        Depth = depth;

        Type innerRequest = request as Type;

        if (innerRequest != null)
        {
            bool nullable = Nullable.GetUnderlyingType(innerRequest) != null;

            ContinueSeed = nullable || innerRequest.IsGenericType;

            if (ContinueSeed)
            {
                GenerationLevel = GetGenerationLevel(innerRequest);
            }
        }
    }

    private int GetGenerationLevel(Type innerRequest)
    {
        int level = 0;

        if (Nullable.GetUnderlyingType(innerRequest) != null)
        {
            level = 1;
        }

        if (innerRequest.IsGenericType)
        {
            foreach (Type generic in innerRequest.GetGenericArguments())
            {
                level++;

                level += GetGenerationLevel(generic);
            }
        }

        return level;
    }
}

public class GenerationDepthGuard : ISpecimenBuilderNode
{
    private readonly ThreadLocal<Stack<DepthSeededRequest>> requestsByThread
        = new ThreadLocal<Stack<DepthSeededRequest>>(() => new Stack<DepthSeededRequest>());

    private Stack<DepthSeededRequest> GetMonitoredRequestsForCurrentThread() => this.requestsByThread.Value;

    public GenerationDepthGuard(ISpecimenBuilder builder)
        : this(builder, EqualityComparer<object>.Default)
    {
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler)
        : this(
            builder,
            depthHandler,
            EqualityComparer<object>.Default,
            1)
    {
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler,
        int generationDepth)
        : this(
            builder,
            depthHandler,
            EqualityComparer<object>.Default,
            generationDepth)
    {
    }

    public GenerationDepthGuard(ISpecimenBuilder builder, IEqualityComparer comparer)
    {
        this.Builder = builder ?? throw new ArgumentNullException(nameof(builder));
        this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
        this.GenerationDepth = 1;
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler,
        IEqualityComparer comparer)
        : this(
        builder,
        depthHandler,
        comparer,
        1)
    {
    }

    public GenerationDepthGuard(
        ISpecimenBuilder builder,
        IGenerationDepthHandler depthHandler,
        IEqualityComparer comparer,
        int generationDepth)
    {
        if (builder == null) throw new ArgumentNullException(nameof(builder));
        if (depthHandler == null) throw new ArgumentNullException(nameof(depthHandler));
        if (comparer == null) throw new ArgumentNullException(nameof(comparer));
        if (generationDepth < 1)
            throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");

        this.Builder = builder;
        this.GenerationDepthHandler = depthHandler;
        this.Comparer = comparer;
        this.GenerationDepth = generationDepth;
    }

    public ISpecimenBuilder Builder { get; }

    public IGenerationDepthHandler GenerationDepthHandler { get; }

    public int GenerationDepth { get; }

    public int CurrentDepth { get; }

    public IEqualityComparer Comparer { get; }

    protected IEnumerable RecordedRequests => this.GetMonitoredRequestsForCurrentThread();

    public virtual object HandleGenerationDepthLimitRequest(object request, int currentDepth)
    {
        return this.GenerationDepthHandler.HandleGenerationDepthLimitRequest(
            request,
            this.GetMonitoredRequestsForCurrentThread(), currentDepth);
    }

    public object Create(object request, ISpecimenContext context)
    {
        if (request is SeededRequest)
        {
            int currentDepth = 0;

            var requestsForCurrentThread = GetMonitoredRequestsForCurrentThread();

            if (requestsForCurrentThread.Count > 0)
            {
                currentDepth = requestsForCurrentThread.Max(x => x.Depth) + 1;
            }

            DepthSeededRequest depthRequest = new DepthSeededRequest(((SeededRequest)request).Request, ((SeededRequest)request).Seed, currentDepth);

            if (depthRequest.Depth >= GenerationDepth)
            {
                var parentRequest = requestsForCurrentThread.Peek();

                depthRequest.MaxDepth = parentRequest.Depth + parentRequest.GenerationLevel;

                if (!(parentRequest.ContinueSeed && currentDepth < depthRequest.MaxDepth))
                {
                    return HandleGenerationDepthLimitRequest(request, depthRequest.Depth);
                }
            }

            requestsForCurrentThread.Push(depthRequest);
            try
            {
                return Builder.Create(request, context);
            }
            finally
            {
                requestsForCurrentThread.Pop();
            }
        }
        else
        {
            return Builder.Create(request, context);
        }
    }

    public virtual ISpecimenBuilderNode Compose(
        IEnumerable<ISpecimenBuilder> builders)
    {
        var composedBuilder = ComposeIfMultiple(
            builders);
        return new GenerationDepthGuard(
            composedBuilder,
            this.GenerationDepthHandler,
            this.Comparer,
            this.GenerationDepth);
    }

    internal static ISpecimenBuilder ComposeIfMultiple(IEnumerable<ISpecimenBuilder> builders)
    {
        ISpecimenBuilder singleItem = null;
        List<ISpecimenBuilder> multipleItems = null;
        bool hasItems = false;

        using (var enumerator = builders.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                singleItem = enumerator.Current;
                hasItems = true;

                while (enumerator.MoveNext())
                {
                    if (multipleItems == null)
                    {
                        multipleItems = new List<ISpecimenBuilder> { singleItem };
                    }

                    multipleItems.Add(enumerator.Current);
                }
            }
        }

        if (!hasItems)
        {
            return new CompositeSpecimenBuilder();
        }

        if (multipleItems == null)
        {
            return singleItem;
        }

        return new CompositeSpecimenBuilder(multipleItems);
    }

    public virtual IEnumerator<ISpecimenBuilder> GetEnumerator()
    {
        yield return this.Builder;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

public class GenerationDepthHandler : IGenerationDepthHandler
{
    public object HandleGenerationDepthLimitRequest(
        object request,
        IEnumerable<object> recordedRequests, int depth)
    {
        return new OmitSpecimen();
    }
}
Elysian answered 1/5, 2018 at 15:12 Comment(3)
Nice try but: Message: AutoFixture.ObjectCreationExceptionWithPath : AutoFixture was unable to create an instance from System.Nullable1[System.DateTime], most likely because it has no public constructor, is an abstract or non-public type. Request path: System.Nullable1[System.DateTime] ReferralDate System.Nullable`1[System.DateTime]Yezd
Sorry about that. It's now edited with the newest version I use.Elysian
Works great I added a method to change the depth after it has been added. So I have it use the default value in a base class, and if a test class needs to adjust it, it can. Thank youPyroxene
C
6

No Bar

One-off:

var f = fixture.Build<Foo>().Without(f => f.Bar).Create();

Reusable:

fixture.Customize<Foo>(c => c.Without(f => f.Bar));
var f = fixture.Create<Foo>();

No Xpto

One-off:

var f = fixture
    .Build<Foo>()
    .With(
        f => f.Bar,
        fixture.Build<Bar>().Without(b => b.Xpto).Create())
    .Create();

Reusable:

fixture.Customize<Bar>(c => c.Without(b => b.Xpto));
var f = fixture.Create<Foo>();
Campfire answered 13/11, 2013 at 15:18 Comment(8)
Yes, I thought someone would answer this, but the goal is not to explicitly omit a certain property/type but to avoid populating an object tree after a specific depth. I will update the example accordingly so that this is more explicit.Hobnob
Then the question is easy to answer, because it's: no, AutoFixture doesn't have such a feature. Why do you need it?Campfire
Sometimes the object graph can be a bit big and in order to avoid loosing time creating a full object tree when you are testing a subtree then that could come in handy, especially when you have no ownership over the class tree itself. Thanks for the answer, by the way, big fan of the AutoFixture project :)Hobnob
I'm sorry, but I don't understand that. In that case, isn't the depth of the graph a completely arbitrary condition to use for pruning, risking that the generated graph is inconsistent or otherwise incorrect?Campfire
Yes it is and I usually generate entire trees or create specialised builders (customisations/behaviours) for subtrees/bounded contexts. But in an edge case I had today where that would make sense (domain I could not change with hundreds of linked classes -- already solved using my default approach) I wondered how hard would it be to do this with the framework and if AutoFixture already had something along these lines that I, for some reason, missed. That's fine. No it does not have that feature. This answers my question, thanks.Hobnob
AutoFixture is pretty extensible, so it might be possible to use the principle of the (already built-in) recursion handling to add such a feature, but I haven't really considered it before now...Campfire
Yes it is. I am currently trying out an implementation of such feature for fun. I will submit it for appreciation when I have something worthwhile. Although it does not follow the standard approach it can be part of an contribution assembly in the future. Let's see how it pans out.Hobnob
Would love to have a .Shallow() to only include non-complex properties.Drivein
S
2

This feature was requested in a github issue. It was ultimately rejected. However, it was rejected because there was a nice, simple solution posted within the issue.

public class GenerationDepthBehavior: ISpecimenBuilderTransformation
{
    public int Depth { get; }

    public GenerationDepthBehavior(int depth)
    {
        Depth = depth;
    }

    public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
    {
        return new RecursionGuard(builder, new OmitOnRecursionHandler(), new IsSeededRequestComparer(), Depth);
    }

    private class IsSeededRequestComparer : IEqualityComparer
    {
        bool IEqualityComparer.Equals(object x, object y)
        {
            return x is SeededRequest && y is SeededRequest;
        }

        int IEqualityComparer.GetHashCode(object obj)
        {
            return obj is SeededRequest ? 0 : EqualityComparer<object>.Default.GetHashCode(obj);
        }
    }
}

You can then use this as follows:

fixture.Behaviors.Add(new GenerationDepthBehavior(2));

Singlehanded answered 8/8, 2019 at 17:49 Comment(2)
I get a weird error on nullable types with this :/Uela
samre here, does not work with nullable typesInnsbruck

© 2022 - 2024 — McMap. All rights reserved.