Creando árbol recursivo con AutoFixture

Resuelto Holstebroe asked hace 11 años • 1 respuestas

Acabo de empezar a utilizar AutoFixture y tengo esta estructura de datos semicompleja para la que me gustaría crear una muestra. En las pruebas con las que estoy trabajando no me importa demasiado el contenido de la estructura de datos. Sólo quiero valores predeterminados razonables.

Parte de esta estructura de datos es un árbol recursivo. Más específicamente, una clase contiene una colección de otra clase que contiene una lista de hijos de sí misma. Algo parecido a:

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

Supongamos que no puedo cambiar fácilmente esta estructura por varias razones.

Si le pido a mi dispositivo que cree A ThrowingRecursionBehavior, comenzará a ladrar acerca de que B es recursivo.

Si reemplazo ThrowingRecursionBehavior con OmitOnRecursionBehavior obtengo una ObjectCreateException.

Si intento algo como: fix.Inject(Enumerable.Empty()); Recibo "Ya se ha agregado un elemento con la misma clave" del DictionaryFiller. Lo mismo sucede si reemplazo ThrowingRecursionBehavior con NullRecursionBehavior.

Hay varias cosas que me gustaría.

  • ¿Cuál sería la mejor manera de crear una muestra de A con una lista vacía de B?
  • ¿Cuál sería la mejor manera de crear un espécimen de A con algunas B que contengan algunos hijos B con algunos hijos (un árbol pequeño)?

Para mi último deseo, podría ser bueno especificar cierta profundidad de recursividad después de la cual se usó Enumerable.Empty (o una matriz/lista de tamaño cero o incluso nula). Sé que AutoFixture es muy flexible de ampliar. Entonces supongo que debería ser posible crear algún constructor de especímenes que haga exactamente esto. De hecho, intentaré jugar con un ISpecimenBuilder personalizado, pero quizás alguien ya tenga una solución más inteligente. Por ejemplo, ¿tendría sentido modificar esta línea en RecursionGuard?

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

a

public object Create(object request, ISpecimenContext context)
{
   if (this.monitoredRequests.Count(x => this.comparer.Equals(x, request)) > maxAllowedRecursions)
   ...
Holstebroe avatar Jun 13 '13 20:06 Holstebroe
Aceptado

Creando A con una lista vacía de B

Es fácil crear una instancia de A con una lista vacía de B:

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

var a = fixture.Create<A>();

Creando un árbol pequeño

Es mucho más difícil crear un árbol pequeño, pero es posible. Ya estás encaminado con tu pensamiento sobre RecursionGuard. Para verificar si esto podría funcionar, copié la mayor parte del código RecursionGuardy lo creé DepthRecursionGuardcomo prueba de concepto :

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

Observe la implementación modificada del Createmétodo, así como el manejo específico de IEnumerable<B>in HandleRecursiveRequest.

Para que esto sea utilizable desde una Fixtureinstancia, también agregué esto DepthRecursionBehavior:

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

Esto me permitió crear un pequeño árbol:

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

Si bien esto es posible, en mi opinión es demasiado difícil, por lo que he creado un elemento de trabajo para hacerlo más fácil en el futuro.


Actualización 2013.11.13: Desde AutoFixture 3.13.0, la profundidad de recursividad se puede configurar a través de esa API.

Mark Seemann avatar Jun 18 '2013 12:06 Mark Seemann