Подтвердить что ты не робот

Создание рекурсивного дерева с помощью AutoFixture

Я только что начал использовать 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 является рекурсивным.

Если я заменил ThrowingRecursionBehavior на OmitOnRecursionBehavior, я получаю исключение ObjectCreateException.

Если я попробую что-то вроде: fixture.Inject(Enumerable.Empty()); Я получаю "Элемент с тем же ключом уже добавлен" из DictionaryFiller. То же самое происходит, если я заменю ThrowingRecursionBehavior на NullRecursionBehavior.

Есть несколько вещей, которые я хотел бы сделать.

  • Что было бы лучшим способом создать образец A с пустым списком Bs?
  • Что было бы лучшим способом создать образец A с несколькими Bs, содержащими несколько B-детей с несколькими детьми (маленькое дерево)?

Для моего последнего желания было бы неплохо указать некоторую глубину рекурсии, после которой был использован Enumerable.Empty(или массив нулевого размера /List или даже 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)
   ...
4b9b3361

Ответ 1

Создание A с пустым списком Bs

Легко создать экземпляр A с пустым списком Bs:

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, а также на конкретную обработку IEnumerable<B> в HandleRecursiveRequest.

Чтобы сделать это пригодным для использования с экземпляром 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>();

Хотя это возможно, это, на мой взгляд, слишком сложно, поэтому я создал рабочий элемент, чтобы упростить его в будущее.


Обновление 2013.11.13: из AutoFixture 3.13.0 глубину рекурсии можно настроить с помощью этого API.