Autofixture: Genera un objeto con restricciones complejas

Creado en 26 ago. 2018  ·  8Comentarios  ·  Fuente: AutoFixture/AutoFixture

Estoy tratando de descubrir la mejor manera de hacer que AutoFixture genere un objeto que tenga restricciones no triviales en el constructor. Por ejemplo, digamos que quiero usar una estructura de datos PrimeNumber que tome un int y acepte solo números primos.

¿Cuál sería el mejor enfoque para generar una instancia de este tipo de estructura en AutoFixture? Quiero decir, obviamente escribiré una personalización, pero ¿qué pondrías allí?

  • ¿Generaría entradas y bucles aleatorios hasta que uno de ellos sea primo (o ejecute un algoritmo generador de primos, por supuesto)? Eso podría ser aceptable para este tipo de restricción, pero si la restricción fuera más difícil de cumplir, rápidamente se volvería costoso.
  • ¿Proporcionaría una lista finita de algunos valores aceptables?

Además, digamos ahora que estoy tratando de crear una instancia de algo que toma varios argumentos que teóricamente pueden ser aleatorios individualmente, pero eso hará alguna validación entre ellos (por ejemplo, argA puede estar en este rango de valores solo si argB es verdadero, y argC debe cumplir con diferentes reglas de validación dependiendo del valor de argA, o la propiedad argC.X tiene que coincidir con la propiedad argA.X, algunas cosas así).

Qué haría usted en este caso ?

  • ¿Una personalización para crear una instancia válida de cada tipo (sin molestarse en ninguna validación externa) y otra que intentaría crear el gran objeto complejo, repitiendo hasta que se crea una instancia válida?
  • Nuevamente, proporcione una lista de valores finitos aceptables, que pueden ser una limitación severa de la amplitud de posibilidades.
  • Proporcionar una personalización especial que crearía solo instancias de argumentos que se ajustarían a la validación del objeto complejo

Y finalmente (podría haber creado varios problemas, pero sentí que todos esos temas son aspectos diferentes del mismo problema), tener que crear y aplicar este tipo de personalizaciones cada vez que agregamos una nueva clase, y tener que mantener esas personalizaciones siempre que el cambio de reglas de validación parece mucho trabajo, ¿aplica algunas técnicas para mitigar esto?

Muchas gracias, perdón por el largo y espero que no sea demasiado desordenado.

question

Comentario más útil

¡Buen día! Finalmente asigné un poco de tipo para responder, perdón por la respuesta enormemente tardía 😊

En primer lugar, preste atención a que el núcleo de AutoFixture es bastante simple y no tenemos soporte incorporado para los árboles complejos con restricciones. En resumen, la estrategia de creación es la siguiente:

  • Busque un constructor público o un método de fábrica estático (método estático que devuelve una instancia del tipo actual).
  • Resuelva los argumentos del constructor y active la instancia.
  • Rellene los campos y las propiedades públicas de escritura con los valores generados.

Con el enfoque actual, como viste anteriormente, de alguna manera no puedes controlar las restricciones de dependencia.

Tenemos algunos puntos de personalización para especificar cómo construir los tipos particulares, pero son relativamente simples y no admiten esas reglas complejas.

¿Cuál sería el mejor enfoque para generar una instancia de este tipo de estructura en AutoFixture? Quiero decir, obviamente escribiré una personalización, pero ¿qué pondrías allí?

  • ¿Generaría entradas y bucles aleatorios hasta que uno de ellos sea primo (o ejecute un algoritmo generador de primos, por supuesto)? Eso podría ser aceptable para este tipo de restricción, pero si la restricción fuera más difícil de cumplir, rápidamente se volvería costoso.

  • ¿Proporcionaría una lista finita de algunos valores aceptables?

Bueno, desafortunadamente no veo una solución milagrosa aquí y el enfoque depende de la situación. Si no confía en que el valor sea demasiado aleatorio, o si un solo SUT consume solo 1-2 números primos, entonces podría estar bien codificar números primos y elegir entre ellos (tenemos ElementsBulider<> asistente incorporado para esos casos). Por otro lado, si necesita una lista grande de números primos y opera con largas secuencias de números primos, entonces probablemente sea mejor codificar un algoritmo para generarlos dinámicamente.

Además, digamos ahora que estoy tratando de crear una instancia de algo que toma varios argumentos que teóricamente pueden ser aleatorios individualmente, pero eso hará alguna validación entre ellos (por ejemplo, argA puede estar en este rango de valores solo si argB es verdadero, y argC debe cumplir con diferentes reglas de validación dependiendo del valor de argA, o la propiedad argC.X tiene que coincidir con la propiedad argA.X, algunas cosas así).

Qué haría usted en este caso ?

Realmente una buena pregunta y desafortunadamente AutoFixture no permite resolverlo de una manera agradable fuera de la caja. Por lo general, trato de aislar las personalizaciones para cada tipo, por lo que la personalización para un tipo controla la creación de un solo tipo. Pero en mis casos los tipos son independientes y obviamente no funcionará bien en tu caso. Además, AutoFixture no proporciona un contexto listo para usar, por lo que cuando escribe una personalización para un tipo en particular, no puede comprender claramente el contexto en el que está creando un objeto (llamado internamente espécimen).

En la parte superior de mi cabeza, diría que normalmente recomendaría la siguiente estrategia:

  • Intente crear personalización para cada tipo de manera que controle la creación de un solo tipo de objeto.
  • Si necesita crear dependencias con restricciones particulares, es mejor activar esas dependencias también en la personalización. Si su dependencia es mutable, puede pedirle a AutoFixture que cree la dependencia para usted y luego la configure de manera que sea compatible.

De esta forma no contradecirás demasiado la arquitectura interna y quedará claro cómo funciona. Por supuesto, potencialmente esta forma es muy detallada.

Si los casos con restricciones complejas no son tan comunes, las capacidades existentes pueden ser suficientes para usted. Pero si su modelo de dominio está realmente lleno de estos casos, francamente, AutoFixture podría no ser la mejor herramienta para usted. Probablemente, existen mejores herramientas en el mercado que permiten resolver este tipo de problemas de la manera más elegante. Por supuesto, vale la pena mencionar que AutoFixture es muy flexible y puede anular casi todo, por lo que siempre puede crear su propio DSL sobre el núcleo de AutoFixture ... Pero debe evaluar cuál es la más barata para usted 😉

También pidamos a @ploeh qué piensa . Por lo general, las respuestas de Mark son profundas e intenta encontrar la causa raíz primero, en lugar de resolver las consecuencias 😅

Si tiene más preguntas, ¡pregunte! Siempre seré bienvenido a responderlas.

PS FWIW, decidí proporcionarle una muestra, donde intenté jugar con AutoFixture y resolver un problema similar (intenté mantenerlo simple y es posible que no funcione del todo en su caso):


Haga clic para ver el código fuente

`` c #
usando el sistema;
usando AutoFixture;
usando AutoFixture.Xunit2;
usando Xunit;

espacio de nombres AutoFixturePlayground
{
clase estática pública Util
{
public static bool IsPrime (número int)
{
// Copiado de https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
''

Todos 8 comentarios

Perdón por el silencio de la radio. Estamos vivos y responderé pronto, estoy extremadamente ocupado en mi trabajo principal estos días. También trabajando en el lanzamiento de NSubstitute v4, por lo que el tiempo es un recurso muy limitado: pensativo: la pregunta es difícil, por lo que debe pensar en todas las formas posibles antes de publicar la respuesta.

Gracias por la paciencia, mantén la melodía: guiño:

Hola,
¿Alguna noticia sobre eso?
Sin presión (conozco el ejercicio 😄, además de que no está bloqueando realmente, solo me gustaría un consejo educado), es solo para saber si tiene algo de visibilidad.
¡Muchas gracias!

¡Buen día! Finalmente asigné un poco de tipo para responder, perdón por la respuesta enormemente tardía 😊

En primer lugar, preste atención a que el núcleo de AutoFixture es bastante simple y no tenemos soporte incorporado para los árboles complejos con restricciones. En resumen, la estrategia de creación es la siguiente:

  • Busque un constructor público o un método de fábrica estático (método estático que devuelve una instancia del tipo actual).
  • Resuelva los argumentos del constructor y active la instancia.
  • Rellene los campos y las propiedades públicas de escritura con los valores generados.

Con el enfoque actual, como viste anteriormente, de alguna manera no puedes controlar las restricciones de dependencia.

Tenemos algunos puntos de personalización para especificar cómo construir los tipos particulares, pero son relativamente simples y no admiten esas reglas complejas.

¿Cuál sería el mejor enfoque para generar una instancia de este tipo de estructura en AutoFixture? Quiero decir, obviamente escribiré una personalización, pero ¿qué pondrías allí?

  • ¿Generaría entradas y bucles aleatorios hasta que uno de ellos sea primo (o ejecute un algoritmo generador de primos, por supuesto)? Eso podría ser aceptable para este tipo de restricción, pero si la restricción fuera más difícil de cumplir, rápidamente se volvería costoso.

  • ¿Proporcionaría una lista finita de algunos valores aceptables?

Bueno, desafortunadamente no veo una solución milagrosa aquí y el enfoque depende de la situación. Si no confía en que el valor sea demasiado aleatorio, o si un solo SUT consume solo 1-2 números primos, entonces podría estar bien codificar números primos y elegir entre ellos (tenemos ElementsBulider<> asistente incorporado para esos casos). Por otro lado, si necesita una lista grande de números primos y opera con largas secuencias de números primos, entonces probablemente sea mejor codificar un algoritmo para generarlos dinámicamente.

Además, digamos ahora que estoy tratando de crear una instancia de algo que toma varios argumentos que teóricamente pueden ser aleatorios individualmente, pero eso hará alguna validación entre ellos (por ejemplo, argA puede estar en este rango de valores solo si argB es verdadero, y argC debe cumplir con diferentes reglas de validación dependiendo del valor de argA, o la propiedad argC.X tiene que coincidir con la propiedad argA.X, algunas cosas así).

Qué haría usted en este caso ?

Realmente una buena pregunta y desafortunadamente AutoFixture no permite resolverlo de una manera agradable fuera de la caja. Por lo general, trato de aislar las personalizaciones para cada tipo, por lo que la personalización para un tipo controla la creación de un solo tipo. Pero en mis casos los tipos son independientes y obviamente no funcionará bien en tu caso. Además, AutoFixture no proporciona un contexto listo para usar, por lo que cuando escribe una personalización para un tipo en particular, no puede comprender claramente el contexto en el que está creando un objeto (llamado internamente espécimen).

En la parte superior de mi cabeza, diría que normalmente recomendaría la siguiente estrategia:

  • Intente crear personalización para cada tipo de manera que controle la creación de un solo tipo de objeto.
  • Si necesita crear dependencias con restricciones particulares, es mejor activar esas dependencias también en la personalización. Si su dependencia es mutable, puede pedirle a AutoFixture que cree la dependencia para usted y luego la configure de manera que sea compatible.

De esta forma no contradecirás demasiado la arquitectura interna y quedará claro cómo funciona. Por supuesto, potencialmente esta forma es muy detallada.

Si los casos con restricciones complejas no son tan comunes, las capacidades existentes pueden ser suficientes para usted. Pero si su modelo de dominio está realmente lleno de estos casos, francamente, AutoFixture podría no ser la mejor herramienta para usted. Probablemente, existen mejores herramientas en el mercado que permiten resolver este tipo de problemas de la manera más elegante. Por supuesto, vale la pena mencionar que AutoFixture es muy flexible y puede anular casi todo, por lo que siempre puede crear su propio DSL sobre el núcleo de AutoFixture ... Pero debe evaluar cuál es la más barata para usted 😉

También pidamos a @ploeh qué piensa . Por lo general, las respuestas de Mark son profundas e intenta encontrar la causa raíz primero, en lugar de resolver las consecuencias 😅

Si tiene más preguntas, ¡pregunte! Siempre seré bienvenido a responderlas.

PS FWIW, decidí proporcionarle una muestra, donde intenté jugar con AutoFixture y resolver un problema similar (intenté mantenerlo simple y es posible que no funcione del todo en su caso):


Haga clic para ver el código fuente

`` c #
usando el sistema;
usando AutoFixture;
usando AutoFixture.Xunit2;
usando Xunit;

espacio de nombres AutoFixturePlayground
{
clase estática pública Util
{
public static bool IsPrime (número int)
{
// Copiado de https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
''

Hola @zvirja

Vaya, gracias por la respuesta detallada, es realmente interesante. Tendré que hacer algunas pruebas y estimar qué vale la pena hacer o no, pero en general esto es genial.

No creo que tenga tantas dependencias que manejar, por lo que su enfoque podría ser un buen camino a seguir. Por supuesto, si @ploeh tiene algo más que agregar, sería un honor para mí 👌

Gracias de nuevo, ¡sigan con el buen trabajo!

Mi experiencia tanto con AutoFixture como con las pruebas basadas en propiedades es que hay esencialmente dos formas de abordar problemas como estos:

  • Filtración
  • Creación algorítmica

(Mientras escribo, mi intuición sugiere que estos podrían ser _catamorfismos_ y _anamorfismos_, respectivamente, pero tendré que pensar en esto un poco más, por lo que este aparte es principalmente una nota para mí).

Si la mayoría de los valores generados aleatoriamente se ajustaran a las restricciones dentro de las que uno debe ajustarse, entonces usar un generador existente, pero descartando el valor inadecuado ocasional, podría ser la forma más fácil de abordar el problema.

Si, por otro lado, un filtro significara desechar la mayoría de los datos aleatorios, tendría que idear un algoritmo que, quizás basado en valores de semilla aleatorios, generará valores que se ajusten a las restricciones en cuestión.

Hace algunos años, di una charla en la que mostraba algunos ejemplos sencillos de ambos enfoques en el contexto de FsCheck . Esta presentación es en realidad una evolución de una charla que adoptó el mismo enfoque, pero solo con AutoFixture en su lugar. Desafortunadamente, no existe ninguna grabación de esa charla.

Se podría abordar el requisito de los números primos de ambas formas.

El enfoque del filtro sería generar números sin restricciones y luego desechar los números hasta obtener uno que sea, de hecho, primo.

El enfoque algorítmico sería utilizar un algoritmo como un tamiz primario para generar un número primo. Sin embargo, esto no es aleatorio, por lo que es posible que desee descubrir cómo aleatorizarlo.

La pregunta general de cómo lidiar con los valores restringidos en AutoFixture surgió casi de inmediato una vez que otras personas comenzaron a buscar en la biblioteca, y escribí un artículo en ese entonces al que todavía me refiero: http://blog.ploeh.dk/2009/ 05/01 / Negociar con entradas restringidas

Con respecto a la cuestión de los múltiples valores que se relacionan entre sí, no deseo dar ninguna orientación general. Ese tipo de preguntas suelen ser problemas XY. En muchos casos, una vez que entiendo los detalles, un diseño alternativo podría resolver problemas no solo con AutoFixture, sino también con la base del código de producción en sí.

Sin embargo, incluso en presencia de problemas XY, todavía habrá situaciones en las que esto puede ser una preocupación legítima, pero prefiero tratarlos caso por caso, ya que, en mi experiencia, son raro.

Entonces, si tiene un ejemplo específico de esto, es posible que pueda ayudar, pero no creo que pueda responder de manera significativa a la pregunta general.

@ploeh Muchas gracias por esta respuesta, que confirma los enfoques que estaba considerando (y me hiciste sentir curiosidad por los cata- y los anamorfismos 😃).
Estoy totalmente de acuerdo en que los valores interdependientes son principalmente un problema XY (al menos en mi caso), la cuestión es que cuando se trabaja en código heredado (no probado 😢), lidiar con esos valores fue un buen comienzo para escribir algunas pruebas de todos modos, hasta que obtengamos el momento de refactorizar esto correctamente.

De todos modos, ambas respuestas abordan el problema bastante bien, supongo que estoy listo para partir de ahí.
¡Gracias!

Por cierto, olvidé mencionar que solo quise decir mi respuesta como una adición a la de @zvirja . Ya es una buena respuesta 👍

No lo tomé de otra manera 😄

¿Fue útil esta página
0 / 5 - 0 calificaciones