【问题标题】:Creating recursive tree with AutoFixture使用 AutoFixture 创建递归树
【发布时间】:2025-06-24 11:05:01
【问题描述】:

我刚刚开始使用 AutoFixture 并拥有我想为其创建一些样本的半复杂数据结构。 在我正在使用的测试中,我不太关心数据结构的内容。我只想要合理的默认值。

此数据结构的一部分是递归树。更具体地说,一个类包含一些其他类的集合,该集合包含其自身的子列表。 类似于:

public class A
{
   private IEnumerable<B> bNodes;
   public A(IEnumerable<B> bNodes)
   {
      this.bNodes = bNodes;
   }
}

public class B
{
   private IEnumerable<B> children;
   public B(IEnumerable<B> children)
   {
      this.children = children;
   }
}

假设由于各种原因我无法轻易更改此结构。

如果我要求我的夹具创建 A ThrowingRecursionBehavior 将开始咆哮 B 是递归的。

如果我用 OmitOnRecursionBehavior 替换 ThrowingRecursionBehavior,我会得到一个 ObjectCreateException。

如果我尝试类似:fixture.Inject(Enumerable.Empty());我从 DictionaryFiller 中得到“已添加具有相同密钥的项目”。如果我将 ThrowingRecursionBehavior 替换为 NullRecursionBehavior,也会发生同样的情况。

有几件事我想做。

  • 用空的 B 列表创建 A 样本的最佳方法是什么?
  • 创建一个包含几个 B 和几个孩子的 A 和几个 B 的样本(一棵小树)的最佳方法是什么?

对于我的最后一个愿望,最好指定一些递归深度,之后使用 Enumerable.Empty(或零大小的数组/列表,甚至为 null)。 我知道 AutoFixture 的扩展非常灵活。所以我想应该有可能创建一些完全做到这一点的样本生成器。 事实上,我会尝试使用自定义的 ISpecimenBuilder,但也许有人已经有了更智能的解决方案。 例如,在 RecursionGuard 中修改这一行是否有意义:

public object Create(object request, ISpecimenContext context)
{
   if (this.monitoredRequests.Any(x => this.comparer.Equals(x, request)))
   ...

public object Create(object request, ISpecimenContext context)
{
   if (this.monitoredRequests.Count(x => this.comparer.Equals(x, request)) > maxAllowedRecursions)
   ...

【问题讨论】:

    标签: c# unit-testing autofixture


    【解决方案1】:

    使用空的 B 列表创建 A

    用空的 B 列表创建 A 的实例很容易:

    var fixture = new Fixture();
    fixture.Inject(Enumerable.Empty<B>());
    
    var a = fixture.Create<A>();
    

    创建一棵小树

    创建一棵小树要困难得多,但它是可能的。您对RecursionGuard 的思考已经步入正轨。为了验证这是否可行,我从RecursionGuard 复制了大部分代码并创建了这个DepthRecursionGuard 作为概念证明

    public class DepthRecursionGuard : ISpecimenBuilderNode
    {
        private readonly ISpecimenBuilder builder;
        private readonly Stack<object> monitoredRequests;
    
        public DepthRecursionGuard(ISpecimenBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException("builder");
            }
    
            this.monitoredRequests = new Stack<object>();
            this.builder = builder;
        }
    
        public object Create(object request, ISpecimenContext context)
        {
            if (this.monitoredRequests.Count(request.Equals) > 1)
                return this.HandleRecursiveRequest(request);
    
            this.monitoredRequests.Push(request);
            var specimen = this.builder.Create(request, context);
            this.monitoredRequests.Pop();
            return specimen;
        }
    
        private object HandleRecursiveRequest(object request)
        {
            if (typeof(IEnumerable<B>).Equals(request))
                return Enumerable.Empty<B>();
    
            throw new InvalidOperationException("boo hiss!");
        }
    
        public ISpecimenBuilderNode Compose(IEnumerable<ISpecimenBuilder> builders)
        {
            var builder = ComposeIfMultiple(builders);
            return new DepthRecursionGuard(builder);
        }
    
        public virtual IEnumerator<ISpecimenBuilder> GetEnumerator()
        {
            yield return this.builder;
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    
        private static ISpecimenBuilder ComposeIfMultiple(
            IEnumerable<ISpecimenBuilder> builders)
        {
            var isSingle = builders.Take(2).Count() == 1;
            if (isSingle)
                return builders.Single();
    
            return new CompositeSpecimenBuilder(builders);
        }
    }
    

    注意Create方法的改变实现,以及HandleRecursiveRequestIEnumerable&lt;B&gt;的具体处理。

    为了使这个Fixture 实例可用,我还添加了这个DepthRecursionBehavior

    public class DepthRecursionBehavior : ISpecimenBuilderTransformation
    {
        public ISpecimenBuilder Transform(ISpecimenBuilder builder)
        {
            return new DepthRecursionGuard(builder);
        }
    }
    

    这使我能够创建一棵小树:

    var fixture = new Fixture();
    fixture.Behaviors.OfType<ThrowingRecursionBehavior>()
        .ToList().ForEach(b => fixture.Behaviors.Remove(b));
    fixture.Behaviors.Add(new DepthRecursionBehavior());
    
    var a = fixture.Create<A>();
    

    虽然这是可能的,但在我看来,这太难了,所以我创建了a work item 以便将来更容易。


    2013.11.13 更新:从 AutoFixture 3.13.0 开始,可以通过该 API 配置递归深度。

    【讨论】:

    • 有趣的是,fixture.Inject(Enumerable.Empty()) 有效,但在我的真实世界代码中做同样的事情会使 DictionaryFiller 失败,并显示“已添加具有相同键的项目” .
    • 如何控制递归深度对我来说不是很明显,你能提供一个例子......
    • 与往常一样,当您第一次弄清楚时非常容易...fixture.Behaviors.Remove(new ThrowingRecursionBehavior()); fixture.Behaviors.Add(new OmitOnRecursionBehavior(1)); //1的递归
    • 看起来不错,虽然我会用fixture.Behaviors.OfType&lt;ThrowingRecursionBehavior&gt;().ToList().ForEach(b =&gt; fixture.Behaviors.Remove(b)); 删除现有行为...
    • DepthRecursionBehavior 不存在。你是说 OmitOnRecursionBehavior 吗?