使用AutoFixture控制对象树的生成深度

12

我正在尝试使用Autofixture控制对象树的生成深度。在某些情况下,我只想生成根对象;而在另一组情况中,我可能希望生成树的深度达到一定程度(比如2、3层)。

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;}
}

结合上面的例子,我希望(深度1)控制生成过程,以便仅实例化Foo类并且不填充Bar属性或该类上的任何其他引用类型,或(深度2)我希望实例化Foo类,并将Bar属性填充为Bar的新实例,但不填充Xpto属性或该类上的任何其他引用类型

如果我在代码库中没有发现Autofixture具有自定义或行为,允许我们拥有那种控制权?

再次强调,我想要控制的不是递归,而是对象图的填充深度


你能分享一下你的代码吗? - Thomas
1
抱歉,马克,这不是因为对象树由不同类型的对象组成。我需要控制的不是递归,而是对象树的填充深度。 - Jay
好的,抱歉。我已经取消了我的关闭投票,并添加了一个答案。 - Mark Seemann
3个回答

7
您可以按以下方式使用 GenerationDepthBehavior 类: 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();
    }
}

不错的尝试,但是出现了以下错误信息:AutoFixture.ObjectCreationExceptionWithPath: AutoFixture 无法从 System.Nullable1[System.DateTime] 创建实例,很可能是因为它没有公共构造函数、是抽象类型或非公共类型。请求路径:System.Nullable1[System.DateTime] ReferralDate System.Nullable`1[System.DateTime]。 - johnstaveley
2
抱歉,现在已经使用最新版本进行编辑。 - Maly Lemire
1
非常好用,我添加了一个方法来在添加后更改深度。因此,我让它使用基类中的默认值,如果测试类需要调整它,则可以这样做。谢谢。 - Ross Degrand

6

无障碍

一次性:

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

可重复利用:

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

无Xpto

一次性:

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

可重复利用:

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

3
是的,我以为会有人回答这个问题,但目标并不是明确省略某个属性/类型,而是避免在特定深度之后填充对象树。我将相应更新示例,以便更加明确。 - Jay
1
那么问题很容易回答,因为它是:“不”,AutoFixture没有这样的功能。你为什么需要它? - Mark Seemann
3
有时候对象图可能会非常庞大,为了避免在测试子树时浪费时间创建完整的对象树,这时候就可以派上用场了,特别是当你无法拥有类树本身时。顺便说一下,很喜欢AutoFixture项目 :) - Jay
是的,我正在尝试实现这样的功能,只是为了好玩。当我有一些值得的东西时,我会提交它以供欣赏。虽然它不遵循标准方法,但它可以成为未来贡献集合的一部分。让我们看看它的效果如何。 - Jay
4
希望有一个.Shallow()方法,只包含非复杂属性。 - Josh M.
显示剩余3条评论

1

这个功能在一个Github问题中被请求。最终它被拒绝了。然而,它被拒绝是因为在问题中发布了一个漂亮、简单的解决方案。

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);
        }
    }
}

你可以按照以下方式使用它: fixture.Behaviors.Add(new GenerationDepthBehavior(2));

1
我在可空类型方面遇到了奇怪的错误 :/ - Kim
同样的问题在这里,不适用可空类型。 - user1029883

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接