¿Cómo escribir pruebas unitarias "buenas"?

61

Activado por este hilo , (nuevamente) estoy pensando finalmente en usar pruebas unitarias en mis proyectos. Algunos carteles dicen algo así como "Las pruebas son geniales, si son buenas pruebas". Mi pregunta ahora: ¿Qué son las pruebas "buenas"?

En mis aplicaciones, la parte principal a menudo es algún tipo de análisis numérico, que depende de grandes cantidades de datos observados y da como resultado una función de ajuste que puede usarse para modelar estos datos. Me resultó especialmente difícil construir pruebas para estos métodos, ya que el número de posibles entradas y resultados es demasiado grande para probar cada caso, y los métodos en sí mismos son a menudo bastante largos y no se pueden refactorizar fácilmente sin sacrificar el rendimiento. Estoy especialmente interesado en pruebas "buenas" para este tipo de método.

Jens
fuente
8
Cualquier buena prueba de unidad solo debe probar una cosa: si falla, debe saber exactamente qué salió mal.
gablin
2
Cuando se tienen grandes cantidades de datos, lo bueno es escribir pruebas genéricas que puedan tomar archivos de datos como entrada. Los archivos de datos generalmente deben contener tanto la entrada como el resultado esperado. Con los marcos de prueba de xunit puede generar casos de prueba sobre la marcha, uno para cada muestra de datos.
froderik
2
@gablin "Si falla, debe saber exactamente qué salió mal" sugeriría que las pruebas con múltiples posibles causas de falla están bien, siempre que pueda determinar la causa a partir de la salida de la prueba ...?
user253751
Nadie parece haber mencionado que las pruebas unitarias pueden probar cuánto tiempo lleva la operación. Puede refactorizar su código teniendo en cuenta el rendimiento, asegurándose de que la prueba de la unidad le diga si pasa o falla en función del tiempo y los resultados.
CJ Dennis

Respuestas:

52

El arte de las pruebas unitarias tiene lo siguiente que decir sobre las pruebas unitarias:

Una prueba unitaria debe tener las siguientes propiedades:

  • Debe ser automatizado y repetible.
  • Debería ser fácil de implementar.
  • Una vez que está escrito, debe permanecer para uso futuro.
  • Cualquiera debería poder ejecutarlo.
  • Debe ejecutarse con solo presionar un botón.
  • Debería correr rápido.

y luego agrega que debería ser completamente automatizado, confiable, legible y mantenible.

Recomiendo leer este libro si aún no lo ha hecho.

En mi opinión, todos estos son muy importantes, pero los tres últimos (confiables, legibles y mantenibles) especialmente, ya que si sus pruebas tienen estas tres propiedades, su código generalmente también las tiene.

Andy Lowry
fuente
1
+1 para una lista completa dirigida a pruebas unitarias (no a pruebas integrales o funcionales)
Gary Rowe
1
+1 para el enlace. Material interesante que se encuentra allí.
Joris Meys
1
"Correr rápido" tiene grandes implicaciones. Es una de las razones por las que las pruebas unitarias deben ejecutarse de forma aislada, lejos de los recursos externos, como la base de datos, el sistema de archivos, el servicio web, etc. Esto, a su vez, genera simulacros / trozos.
Michael Pascua
1
cuando dice It should run at the push of a button, ¿eso significa que una prueba unitaria no debería requerir que se ejecuten contenedores (servidor de aplicaciones) (para la unidad que se está probando) o una conexión de recursos (como DB, servicios web externos, etc.)? Estoy confundido acerca de qué partes de una aplicación deben ser probadas y cuáles no. Me han dicho que las pruebas unitarias no deberían depender de la conexión a la base de datos y de los contenedores en ejecución, y tal vez usaron maquetas en su lugar.
anfibio
42

Una buena prueba unitaria no refleja la función que está probando.

Como un ejemplo muy simplificado, considere que tiene una función que devuelve un promedio de dos int. La prueba más completa llamaría a la función y verificaría si un resultado es en realidad un promedio. Esto no tiene ningún sentido: está reflejando (replicando) la funcionalidad que está probando. Si cometió un error en la función principal, cometerá el mismo error en la prueba.

En otras palabras, si te encuentras replicando la funcionalidad principal en la prueba de la unidad, es una señal probable de que estás perdiendo el tiempo.

mojuba
fuente
21
+1 Lo que haría en este caso es probar con argumentos codificados y verificar su respuesta conocida.
Michael K
He visto ese olor antes.
Paul Butcher
¿Podría dar un ejemplo de una buena prueba de unidad para la función que devuelve promedios?
VLAS
2
@VLAS prueba valores predefinidos, por ejemplo, asegúrese de que avg (1, 3) == 2, también más importante, verifique los casos límite, como INT_MAX, ceros, valores negativos, etc. Si se encontró un error y se solucionó en la función, agregue otro prueba para asegurarse de que este error nunca se reintroduzca.
mojuba
Interesante. ¿Cómo propone obtener las respuestas correctas a esas entradas de prueba y no cometer el mismo error que el código sometido a la prueba?
Timo
10

Las buenas pruebas unitarias son esencialmente la especificación en forma ejecutable:

  1. Describir el comportamiento del código correspondiente a los casos de uso.
  2. cubrir casos de esquina técnica (qué sucede si se pasa nulo): si no hay una prueba para un caso de esquina, el comportamiento es indefinido.
  3. romper si el código probado cambia fuera de la especificación

He descubierto que Test-Driven-Development es muy adecuado para las rutinas de la biblioteca, ya que esencialmente escribe primero la API y, luego, la implementación real.


fuente
7

para TDD, las "buenas" pruebas prueban las características que el cliente desea ; las características no se corresponden necesariamente con las funciones, y el desarrollador no debe crear escenarios de prueba en el vacío

en su caso, supongo, la 'característica' es que la función de ajuste modela los datos de entrada dentro de una cierta tolerancia a errores. Como no tengo idea de lo que realmente estás haciendo, estoy inventando algo; Ojalá sea análogo.

Ejemplo de historia:

Como [X-Wing Pilot] quiero [no más de 0.0001% de error de ajuste] para que [la computadora objetivo pueda alcanzar el puerto de escape de la Estrella de la Muerte cuando se mueva a toda velocidad a través de un cañón de caja]

Entonces vaya a hablar con los pilotos (y con la computadora de orientación, si es consciente). Primero hablas de lo que es "normal", luego hablas de lo anormal. Descubre lo que realmente importa en este escenario, lo que es común, lo que es poco probable y lo que es simplemente posible.

Digamos que normalmente tendrá una ventana de medio segundo sobre siete canales de datos de telemetría: velocidad, cabeceo, balanceo, guiñada, vector objetivo, tamaño objetivo y velocidad objetivo, y que estos valores serán constantes o cambiarán linealmente. Anormalmente puede tener menos canales y / o los valores pueden estar cambiando rápidamente. Así que juntos se les ocurren algunas pruebas como:

//Scenario 1 - can you hit the side of a barn?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is zero
    and all other values are constant,
Then:
    the error coefficient must be zero

//Scenario 2 - can you hit a turtle?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is less than c
    and all other values are constant,
Then:
    the error coefficient must be less than 0.0000000001/ns

...

//Scenario 42 - death blossom
Given:
    all 7 channels with 30% dropout and a 0.05 second sampling window
When:
    speed is zero
    and position is within enemy cluster
    and all targets are stationary
Then:
    the error coefficient must be less than 0.000001/ns for each target

Ahora, es posible que haya notado que no hay un escenario para la situación particular descrita en la historia. Resulta que, después de hablar con el cliente y otras partes interesadas, ese objetivo en la historia original era solo un ejemplo hipotético. Las pruebas reales salieron de la discusión que siguió. Esto puede suceder. La historia debe reescribirse, pero no tiene que ser así [ya que la historia es solo un marcador de posición para una conversación con el cliente].

Steven A. Lowe
fuente
5

Cree pruebas para casos de esquina, como un conjunto de pruebas que contenga solo el número mínimo de entradas (posible 1 o 0) y algunos casos estándar. Esas pruebas unitarias no son un reemplazo para las pruebas de aceptación exhaustivas, ni deberían serlo.

usuario281377
fuente
5

He visto muchos casos en los que las personas invierten una enorme cantidad de esfuerzo escribiendo pruebas para el código que rara vez se ingresa, y no escribiendo pruebas para el código que se ingresa con frecuencia.

Antes de sentarse a escribir cualquier prueba, debe mirar algún tipo de gráfico de llamadas para asegurarse de planificar una cobertura adecuada.

Además, no creo en escribir pruebas solo por decir "Sí, lo probamos". Si estoy usando una biblioteca que se deja caer y permanecerá inmutable, no voy a perder el día escribiendo pruebas para asegurarme de que las entrañas de una API que nunca cambiará funciona como se esperaba, incluso si ciertas partes de ella califican alto en un gráfico de llamadas. Las pruebas que consumen dicha biblioteca (mi propio código) señalan esto.

Tim Post
fuente
pero, ¿qué sucede en una fecha posterior cuando la biblioteca tiene una versión más nueva con una corrección de errores?
@ Thorbjørn Ravn Andersen: depende de la biblioteca, de lo que cambió y de su propio proceso de prueba. No voy a escribir pruebas para el código que sé que funciona cuando lo coloqué en su lugar, y nunca lo toco. Entonces, si funciona después de la actualización, fuera de la mente desaparece :) Por supuesto, hay excepciones.
Tim Post
si depende de su biblioteca, lo menos que puede hacer es escribir pruebas que muestren lo que espera que dicha biblioteca haga en realidad ,
... y si eso cambió, prueba en cosas que consumen dicha biblioteca ... tl; dr; No necesito probar las entrañas del código de terceros. Sin embargo, la respuesta se actualizó para mayor claridad.
Tim Post
4

No del todo TDD, pero después de haber entrado en QA, puede mejorar sus pruebas configurando casos de prueba para reproducir cualquier error que surja durante el proceso de QA. Esto puede ser particularmente valioso cuando se busca asistencia a más largo plazo y se comienza a llegar a un lugar donde se arriesga a que las personas reintroduzcan errores viejos sin darse cuenta. Tener una prueba para capturar eso es particularmente valioso.

glenatron
fuente
3

Intento que cada prueba solo pruebe una cosa. Intento dar a cada prueba un nombre como shouldDoSomething (). Intento probar el comportamiento, no la implementación. Solo pruebo métodos públicos.

Por lo general, tengo una o algunas pruebas de éxito, y luego tal vez un puñado de pruebas de fracaso, por método público.

Yo uso muchas maquetas. Un buen marco simulado probablemente sería bastante útil, como PowerMock. Aunque todavía no estoy usando ninguno.

Si la clase A usa otra clase B, agregaría una interfaz, X, para que A no use B directamente. Luego crearía una maqueta XMockup y la usaría en lugar de B en mis pruebas. Realmente ayuda a acelerar la ejecución de la prueba, reduciendo la complejidad de la prueba, y también reduce el número de pruebas que escribo para A ya que no tengo que hacer frente a las peculiaridades de B. Puedo, por ejemplo, probar que A llama a X.someMethod () en lugar de un efecto secundario de llamar a B.someMethod ().

Mantenga su código de prueba limpio también.

Al usar una API, como una capa de base de datos, me burlaría de ella y permitiría que la maqueta arroje una excepción en cada oportunidad posible en el comando. Luego ejecuto las pruebas una sin lanzar, y en un bucle, cada vez que lanzo una excepción en la próxima oportunidad hasta que la prueba tenga éxito nuevamente. Un poco como las pruebas de memoria disponibles para Symbian.

Roger CS Wernersson
fuente
2

Veo que Andry Lowry ya ha publicado las métricas de prueba de unidad de Roy Osherove; pero parece que nadie ha presentado el conjunto (complementario) que ofrece el tío Bob en Clean Code (132-133). Él usa el acrónimo PRIMERO (aquí con mis resúmenes):

  • Rápido (deberían correr rápido, para que a la gente no le importe correr)
  • Independiente (las pruebas no deben hacer configuración o desmontaje el uno para el otro)
  • Repetible (debe ejecutarse en todos los entornos / plataformas)
  • Autovalidante (totalmente automatizado; la salida debe ser "pasar" o "fallar", no un archivo de registro)
  • A tiempo (cuándo escribirlos, justo antes de escribir el código de producción que prueban)
Kazark
fuente