Cómo unificar el método de prueba que devuelve una colección mientras se evita la lógica en la prueba

14

Estoy probando un método que es generar una colección de objetos de datos. Quiero verificar que las propiedades de los objetos se estén configurando correctamente. Algunas de las propiedades se establecerán en la misma cosa; otros se establecerán en un valor que depende de su posición en la colección. La forma natural de hacer esto parece ser con un bucle. Sin embargo, Roy Osherove recomienda enfáticamente no usar la lógica en las pruebas unitarias ( Art of Unit Testing , 178). Él dice:

Una prueba que contiene lógica generalmente está probando más de una cosa a la vez, lo que no se recomienda, porque la prueba es menos legible y más frágil. Pero la lógica de prueba también agrega complejidad que puede contener un error oculto.

Las pruebas deberían, como regla general, ser una serie de llamadas a métodos sin flujos de control, ni siquiera try-catch, y con llamadas de afirmación.

Sin embargo, no puedo ver nada malo en mi diseño (¿de qué otra manera genera una lista de objetos de datos, algunos de cuyos valores dependen de en qué lugar de la secuencia se encuentran? No se pueden generar y probar exactamente por separado). ¿Hay algo que no sea amigable con mi diseño? ¿O estoy siendo demasiado rígidamente dedicado a la enseñanza de Osherove? ¿O hay alguna magia secreta de prueba de unidad que no conozco que evita este problema? (Estoy escribiendo en C # / VS2010 / NUnit, pero si es posible estoy buscando respuestas independientes del lenguaje).

Kazark
fuente
44
Recomiendo no hacer bucles. Si su prueba es que la tercera cosa tiene su Barra establecida en Frob, entonces escriba una prueba para verificar específicamente que la Barra de la tercera cosa es Frob. Esa es una prueba en sí misma, vaya directamente a ella, sin bucle. Si su prueba es que obtiene una colección de 5 cosas, esa también es una prueba. Eso no quiere decir que nunca tenga un bucle (explícito o no), es solo que a menudo no es necesario. Además, trate el libro de Osherove como más pautas que reglas reales.
Anthony Pegram,
1
@AnthonyPegram Sets no están ordenados: Frob a veces puede ser tercero, a veces puede ser segundo. No puede confiar en él, haciendo necesario un bucle (o una función de lenguaje como Python in), si la prueba es "Frob se agregó con éxito a una colección existente".
Izkata
1
@Izbata, su pregunta menciona específicamente que ordenar es importante. Sus palabras: "otros se establecerán en un valor que depende de su posición en la colección". Hay muchos tipos de colecciones en C # (el lenguaje al que hace referencia) que están ordenadas por inserción. Para el caso, también puede confiar en el orden con listas en Python, un lenguaje que menciona.
Anthony Pegram,
Además, supongamos que está probando un método de reinicio en una colección. Debe recorrer la colección y verificar cada elemento. Dependiendo del tamaño de la colección, no probarlo en un bucle es ridículo. O digamos que estoy probando algo que se supone que incrementa cada elemento de una colección. Puede configurar todos los elementos con el mismo valor, llamar a su incremento y luego verificar. Esa prueba apesta. Debe establecer varios de ellos en diferentes valores, aumentar la llamada y verificar que todos los diferentes valores se incrementaron correctamente. Marcar solo un elemento aleatorio en la colección está dejando mucho al azar.
iheanyi
No voy a responder de esta manera porque obtendré miles de millones de votos negativos, pero a menudo solo comparto toString()la Colección y la comparo con lo que debería ser. Simple y funciona.
user949300 el

Respuestas:

16

TL; DR:

  • Escribe el examen
  • Si la prueba hace demasiado, el código también puede hacer demasiado.
  • Puede que no sea una prueba unitaria (pero no una mala prueba).

Lo primero para probar es que el dogma no es útil. Me gusta leer The Way of Testivus, que señala algunos problemas con el dogma de una manera alegre.

Escriba el examen que necesita ser escrito.

Si la prueba necesita ser escrita de alguna manera, escríbala de esa manera. Intentar forzar la prueba en un diseño de prueba idealizado o no tenerla no es algo bueno. Tener un examen hoy que lo pruebe es mejor que tener un examen "perfecto" algún día más tarde.

También señalaré el bit en la prueba fea:

Cuando el código es feo, las pruebas pueden ser feas.

No le gusta escribir pruebas feas, pero el código feo necesita más pruebas.

No dejes que el código feo te impida escribir pruebas, pero deja que el código feo te impida escribir más.

Estos pueden considerarse truismos para aquellos que han estado siguiendo durante mucho tiempo ... y simplemente se arraigan en la forma de pensar y escribir pruebas. Para las personas que no han estado y están tratando de llegar a ese punto, los recordatorios pueden ser útiles (incluso encuentro que releerlos me ayuda a evitar quedar atrapado en algún dogma).


Tenga en cuenta que al escribir una prueba fea, si el código puede ser una indicación de que el código también está tratando de hacer demasiado. Si el código que está probando es demasiado complejo para ejercerlo correctamente escribiendo una prueba simple, es posible que desee considerar dividir el código en partes más pequeñas que se puedan probar con las pruebas más simples. No se debe escribir una prueba unitaria que haga todo (podría no ser una prueba unitaria entonces). Del mismo modo que los 'objetos de Dios' son malos, las 'pruebas de unidad de Dios' también son malas y deberían ser indicaciones para volver y mirar el código nuevamente.

Usted debe ser capaz de ejercer todo el código con una cobertura razonable a través de tales pruebas sencillas. Las pruebas que hacen más pruebas de extremo a extremo que se ocupan de preguntas más grandes ("Tengo este objeto, ordenado en xml, enviado al servicio web, a través de las reglas, de regreso y sin ordenar") es una prueba excelente, pero ciertamente no lo es Es una prueba unitaria (y cae en el ámbito de las pruebas de integración, incluso si se ha burlado de los servicios que llama y los ha personalizado en las bases de datos de memoria para hacer las pruebas). Todavía puede usar el marco XUnit para las pruebas, pero el marco de prueba no lo convierte en una prueba unitaria.


fuente
7

Estoy agregando una nueva respuesta porque mi perspectiva es diferente de cuando escribí la pregunta y la respuesta original; no tiene sentido juntarlos en uno solo.

Dije en la pregunta original

Sin embargo, no puedo ver nada malo con mi diseño (¿de qué otra manera se genera una lista de objetos de datos, algunos de cuyos valores dependen de en qué parte de la secuencia están? No se pueden generar y probar por separado)

Aquí es donde me equivoqué. Después de hacer una programación funcional durante el último año, ahora me doy cuenta de que solo necesitaba una operación de recolección con un acumulador. Entonces podría escribir mi función como una función pura que operaba en una cosa y usar alguna función de biblioteca estándar para aplicarla a la colección.

Entonces, mi nueva respuesta es: use técnicas de programación funcional y evitará este problema la mayor parte del tiempo. Puede escribir sus funciones para operar en cosas individuales y solo aplicarlas a colecciones de cosas en el último momento. Pero si son puros, puede probarlos sin referencia a las colecciones.

Para una lógica más compleja, apóyate en pruebas basadas en propiedades . Cuando tienen lógica, debe ser menor e inversa a la lógica del código bajo prueba, y cada prueba verifica mucho más que una prueba unitaria basada en casos, por lo que vale la pena la pequeña cantidad de lógica.

Sobre todo, siempre apóyate en tus tipos . Obtenga los tipos más fuertes que pueda y úselos para su ventaja. Esto reducirá la cantidad de pruebas que tiene que escribir en primer lugar.

Kazark
fuente
4

No intentes probar demasiadas cosas a la vez. Cada una de las propiedades de cada objeto de datos en la colección es demasiado para una prueba. En cambio, recomiendo:

  1. Si la colección es de longitud fija, escriba una prueba unitaria para validar la longitud. Si es de longitud variable, escriba varias pruebas de longitudes que caractericen su comportamiento (por ejemplo, 0, 1, 3, 10). De cualquier manera, no valide las propiedades en estas pruebas.
  2. Escriba una prueba unitaria para validar cada una de las propiedades. Si la colección es de longitud fija y corta, simplemente afirme contra una propiedad de cada uno de los elementos para cada prueba. Si es de longitud fija pero larga, elija una muestra representativa pero pequeña de los elementos para afirmar contra una propiedad cada uno. Si es de longitud variable, genere una colección relativamente corta pero representativa (es decir, quizás tres elementos) y haga valer una propiedad de cada uno.

Hacerlo de esta manera hace que las pruebas sean lo suficientemente pequeñas como para que dejar bucles no parezca doloroso. C # / Ejemplo de unidad, método dado bajo prueba ICollection<Foo> generateFoos(uint numberOfFoos):

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

Si está acostumbrado al paradigma de "prueba de unidad plana" (sin estructuras / lógica anidadas), estas pruebas parecen bastante claras. Por lo tanto, se evita la lógica en las pruebas al identificar el problema original como intentar probar demasiadas propiedades a la vez, en lugar de carecer de bucles.

Kazark
fuente
1
Osherove tendría tu cabeza en un plato por tener 3 afirmaciones. ;) El primero en fallar significa que nunca validará el resto. Tenga en cuenta también que realmente no evitó el bucle. Simplemente lo expandió explícitamente a su forma ejecutada. No es una crítica dura, sino solo una sugerencia para obtener más práctica aislando sus casos de prueba a la cantidad mínima posible, para recibir comentarios más específicos cuando algo falla, mientras continúa validando otros casos que posiblemente podrían pasar (o fallar, con sus propios comentarios específicos).
Anthony Pegram
3
@AnthonyPegram Sé sobre el paradigma de una afirmación por prueba. Prefiero el mantra "prueba de una cosa" (como propugna Bob Martin, en contra de una afirmación por prueba, en Clean Code ). Nota al margen: los marcos de prueba de unidad que tienen "esperar" versus "afirmar" son buenos (Pruebas de Google). En cuanto al resto, ¿por qué no divide sus sugerencias en una respuesta completa, con ejemplos? Creo que podría beneficiarme.
Kazark