Tipos de pruebas unitarias basadas en la utilidad.

13

Desde el punto de vista del valor, veo dos grupos de pruebas unitarias en mi práctica:

  1. Pruebas que prueban alguna lógica no trivial. Escribirlos (antes o después de la implementación) revela algunos problemas / errores potenciales y ayuda a tener confianza en caso de que la lógica cambie en el futuro.
  2. Pruebas que prueban alguna lógica muy trivial. Esas pruebas se parecen más al código del documento (generalmente con simulacros) que a la prueba. El flujo de trabajo de mantenimiento de esas pruebas no es "alguna lógica cambiada, la prueba se volvió roja - gracias a Dios que escribí esta prueba" sino "algún código trivial cambiado, la prueba se volvió falsamente negativa - Tengo que mantener (reescribir) la prueba sin obtener ningún beneficio" . La mayoría de las veces no vale la pena mantener esas pruebas (excepto por razones religiosas). Y según mi experiencia en muchos sistemas, esas pruebas son como el 80% de todas las pruebas.

Estoy tratando de averiguar qué piensan los demás sobre el tema de la separación de pruebas unitarias por valor y cómo corresponde a mi separación. Pero lo que más veo es propaganda TDD a tiempo completo o propaganda de pruebas-son-inútiles-solo-escribir-el-código. Estoy interesado en algo en el medio. Sus propios pensamientos o referencias a artículos / artículos / libros son bienvenidos.

SiberianGuy
fuente
3
Mantengo las pruebas unitarias buscando errores conocidos (específicos), que una vez pasaron por el conjunto de pruebas unitarias originales, como un grupo separado cuya función es prevenir errores de regresión.
Konrad Morawski
66
Ese segundo tipo de pruebas es lo que yo veo como una especie de "cambio de fricción". No descarte su utilidad. Cambiar incluso las trivialidades del código tiende a tener efectos de onda en toda la base de código, y la introducción de este tipo de fricción actúa como un obstáculo para sus desarrolladores, de modo que solo cambian las cosas que realmente lo necesitan, en lugar de basarse en alguna preferencia personal o personal.
Telastyn
3
@Telastyn - Todo sobre tu comentario me parece completamente loco. ¿Quién deliberadamente dificultaría el cambio de código? ¿Por qué desalentar a los desarrolladores de cambiar el código como mejor les parezca? ¿No confían en ellos? ¿Son malos desarrolladores?
Benjamin Hodgson
2
En cualquier caso, si cambiar el código tiende a tener "efectos dominó", entonces su código tiene un problema de diseño , en cuyo caso se debe alentar a los desarrolladores a refactorizar tanto como sea razonable. Las pruebas frágiles desalientan activamente la refactorización (una prueba falla; ¿quién puede molestarse en averiguar si esa prueba fue una de las 80% de las pruebas que realmente no hacen nada? Simplemente encuentra una forma diferente y más complicada de hacerlo). Pero parece ver esto como una característica deseable ... No lo entiendo en absoluto.
Benjamin Hodgson
2
De todos modos, el OP puede encontrar interesante esta publicación del blog del creador de Rails. Para simplificar demasiado su punto, probablemente deberías intentar tirar ese 80% de las pruebas.
Benjamin Hodgson

Respuestas:

14

Creo que es natural encontrar una división dentro de las pruebas unitarias. Hay muchas opiniones diferentes sobre cómo hacerlo correctamente y, naturalmente, todas las demás opiniones son intrínsecamente incorrectas . Hay bastantes artículos sobre DrDobbs recientemente que exploran este mismo problema al que enlazo al final de mi respuesta.

El primer problema que veo con las pruebas es que es fácil equivocarse. En mi clase universitaria de C ++ estuvimos expuestos a pruebas unitarias tanto en el primer como en el segundo semestre. No sabíamos nada acerca de la programación en general en ninguno de los semestres; estábamos tratando de aprender los fundamentos de la programación a través de C ++. Ahora imagine decirles a los estudiantes: "¡Oh, oye, escribiste una pequeña calculadora de impuestos anual! Ahora escribe algunas pruebas unitarias para asegurarte de que funciona correctamente". Los resultados deberían ser obvios: todos fueron horribles, incluidos mis intentos.

Una vez que admites que apestas al escribir pruebas unitarias y deseas mejorar, pronto te enfrentarás a estilos de prueba modernos o diferentes metodologías. Al probar metodologías, me refiero a prácticas como test-first o lo que hace Andrew Binstock de DrDobbs, que es escribir las pruebas junto con el código. Ambos tienen sus pros y sus contras, y me niego a entrar en detalles subjetivos porque eso provocará una guerra de llamas. Si no está confundido acerca de qué metodología de programación es mejor, entonces quizás el estilo de prueba sea suficiente. ¿Debería usar TDD, BDD, pruebas basadas en propiedades? JUnit tiene conceptos avanzados llamados Teorías que desdibujan la línea entre TDD y las pruebas basadas en propiedades. ¿Cuál usar cuando?

tl; dr Es fácil equivocarse en las pruebas, es increíblemente obstinado y no creo que ninguna metodología de prueba sea inherentemente mejor siempre que se utilicen diligentemente y profesionalmente en el contexto en el que son apropiadas. Además, la prueba es En mi opinión, una extensión de las afirmaciones o pruebas de cordura que solían garantizar un enfoque de desarrollo ad-hoc a prueba de fallos que ahora es mucho, mucho más fácil.

Para una opinión subjetiva, prefiero escribir "fases" de pruebas, a falta de una mejor frase. Escribo pruebas unitarias que evalúan las clases de forma aislada, usando simulacros cuando es necesario. Probablemente se ejecutarán con JUnit o algo similar. Luego escribo pruebas de integración o aceptación, estas se ejecutan por separado y generalmente solo unas pocas veces al día. Este es su caso de uso no trivial. Usualmente uso BDD ya que es bueno expresar características en lenguaje natural, algo que JUnit no puede proporcionar fácilmente.

Por último, los recursos. Estos presentarán opiniones contradictorias centradas principalmente en las pruebas unitarias en diferentes idiomas y con diferentes marcos. Deben presentar la división en ideología y metodología mientras te permiten inventar tu propia opinión siempre y cuando no haya manipulado demasiado la tuya :)

[1] La corrupción de Agile por Andrew Binstock

[2] Respuesta a las respuestas del artículo anterior

[3] Respuesta a la corrupción de Agile por el tío Bob

[4] Respuesta a la corrupción de Agile por Rob Myers

[5] ¿Por qué molestarse con las pruebas de pepino?

[6] Lo estás haciendo mal

[7] Aléjate de las herramientas

[8] Comentario sobre 'Kata de números romanos con comentario'

[9] Números romanos Kata con comentario

IAE
fuente
1
Una de mis afirmaciones amistosas sería que si está escribiendo una prueba para probar la función de una calculadora de impuestos anual, entonces no está escribiendo una prueba unitaria. Esa es una prueba de integración. Su calculadora debe desglosarse en unidades de ejecución bastante simples, y sus pruebas unitarias luego prueban esas unidades. Si una de esas unidades deja de funcionar correctamente (la prueba comienza a fallar), entonces es como derribar parte de una pared de cimientos y necesita reparar el código (no la prueba, en general). O eso, o has identificado un poco de código que ya no es necesario y debe descartarse.
Craig
1
@ Craig: ¡Precisamente! Esto es lo que quise decir con no saber cómo escribir las pruebas adecuadas. Como estudiante universitario, el recaudador de impuestos era una gran clase escrita sin una comprensión adecuada de SOLID. Tiene toda la razón al pensar que se trata más de una prueba de integración que de otra cosa, pero ese era un término desconocido para nosotros. Solo fuimos expuestos a pruebas de "unidad" por nuestro profesor.
IAE
5

Creo que es importante tener pruebas de ambos tipos y usarlas cuando sea apropiado.

Como dijiste, hay dos extremos y, sinceramente, no estoy de acuerdo con ninguno de los dos.

La clave es que las pruebas unitarias deben cubrir las reglas y requisitos comerciales . Si hay un requisito de que el sistema debe rastrear la edad de una persona, escriba pruebas "triviales" para asegurarse de que la edad sea un número entero no negativo. Está probando el dominio de datos requerido por el sistema: si bien es trivial, tiene valor porque impone los parámetros del sistema .

Del mismo modo con pruebas más complejas, tienen que aportar valor. Claro, puede escribir una prueba que valide algo que no es un requisito, pero que debe aplicarse en una torre de marfil en algún lugar, pero que es el mejor momento para escribir pruebas que validen los requisitos por los cuales el cliente le está pagando. Por ejemplo, ¿por qué escribir una prueba que valida su código puede tratar con una secuencia de entrada que agota el tiempo de espera, cuando las únicas secuencias son de archivos locales, no de la red?

Creo firmemente en las pruebas unitarias y uso TDD donde sea que tenga sentido. Las pruebas unitarias ciertamente aportan valor en forma de una mayor calidad y un comportamiento de "falla rápida" al cambiar el código. Sin embargo, también hay que tener en cuenta la antigua regla 80/20 . En algún momento alcanzará rendimientos decrecientes al escribir pruebas, y debe pasar a un trabajo más productivo, incluso si hay algún valor medible al escribir más pruebas.


fuente
Escribir una prueba para garantizar que un sistema rastrea la edad de una persona no es una prueba unitaria, IMO. Esa es una prueba de integración. Una prueba unitaria probaría la unidad genérica de ejecución (también conocido como "procedimiento") que, por ejemplo, calcula un valor de edad a partir de, por ejemplo, una fecha base y una compensación en cualquier unidad (días, semanas, etc.). Mi punto es que un poco de código no debería tener ninguna dependencia externa extraña en el resto del sistema. SOLO calcula una edad a partir de un par de valores de entrada, y en ese caso una prueba unitaria puede confirmar el comportamiento correcto, lo que probablemente arroje una excepción si el desplazamiento produce una edad negativa.
Craig
No me refería a ningún cálculo. Si un modelo almacena una pieza de datos, puede validar que los datos pertenecen al dominio correcto. En este caso, el dominio es el conjunto de enteros no negativos. Los cálculos deben realizarse en el controlador (en MVC), y en este ejemplo un cálculo de edad sería una prueba separada.
4

Aquí está mi opinión: todas las pruebas tienen costos:

  • tiempo inicial y esfuerzo:
    • pensar en qué probar y cómo probarlo
    • implementar la prueba y asegurarse de que está probando lo que se supone que debe
  • Mantenimiento en proceso
    • asegurándose de que la prueba siga haciendo lo que se supone que debe hacer a medida que el código evoluciona naturalmente
  • ejecutando la prueba
    • Tiempo de ejecución
    • analizando los resultados

También tenemos la intención de que todas las pruebas brinden beneficios (y en mi experiencia, casi todas las pruebas brindan beneficios):

  • especificación
  • resaltar casos de esquina
  • prevenir la regresión
  • verificación automática
  • ejemplos de uso de API
  • cuantificación de propiedades específicas (tiempo, espacio)

Así que es bastante fácil ver que si escribes un montón de pruebas, probablemente tendrán algún valor. Cuando esto se complica es cuando comienzas a comparar ese valor (que, por cierto, es posible que no lo sepas de antemano; si arrojas tu código, las pruebas de regresión pierden su valor) con el costo.

Ahora, su tiempo y esfuerzo son limitados. Le gustaría elegir hacer las cosas que proporcionan el mayor beneficio al menor costo. Y creo que es algo muy difícil de hacer, sobre todo porque puede requerir el conocimiento de que uno no tiene o sería costoso obtenerlo.

Y ese es el verdadero problema entre estos diferentes enfoques. Creo que todos han identificado estrategias de prueba que son beneficiosas. Sin embargo, cada estrategia tiene diferentes costos y beneficios en general. Además, los costos y beneficios de cada estrategia probablemente dependerán en gran medida de los detalles del proyecto, el dominio y el equipo. En otras palabras, puede haber múltiples mejores respuestas.

En algunos casos, extraer código sin pruebas puede proporcionar los mejores beneficios / costos. En otros casos, un conjunto completo de pruebas puede ser mejor. En otros casos, mejorar el diseño puede ser lo mejor que se puede hacer.


fuente
2

¿Qué es realmente una prueba unitaria ? ¿Y hay realmente una gran dicotomía en juego aquí?

Trabajamos en un campo donde leer literalmente un poco más allá del final de un búfer puede bloquear completamente un programa, o producir un resultado totalmente inexacto, o como lo demuestra el reciente error TLS "HeartBleed", pone un sistema supuestamente seguro abrir sin producir ninguna evidencia directa de la falla.

Es imposible eliminar toda la complejidad de estos sistemas. Pero nuestro trabajo es, en la medida de lo posible, minimizar y gestionar esa complejidad.

¿Es una prueba unitaria una prueba que confirma, por ejemplo, que una reserva se ha publicado con éxito en tres sistemas diferentes, se crea una entrada de registro y se envía una confirmación por correo electrónico?

Voy a decir que no . Esa es una prueba de integración . Y esos definitivamente tienen su lugar, pero también son un tema diferente.

Una prueba de integración funciona para confirmar la función general de una "característica" completa. Pero el código detrás de esa característica debe desglosarse en bloques de construcción simples y comprobables, también conocidos como "unidades".

Por lo tanto, una prueba unitaria debe tener un alcance muy limitado.

Lo que implica que el código probado por la prueba unitaria debe tener un alcance muy limitado.

Lo que implica además que uno de los pilares del buen diseño es dividir su complejo problema en piezas más pequeñas y de un solo propósito (en la medida de lo posible) que se pueden probar en relativo aislamiento entre sí.

Lo que termina es un sistema hecho de componentes básicos confiables, y usted sabe si alguna de esas unidades fundamentales de código se rompe porque ha escrito pruebas simples, pequeñas y de alcance limitado para decirle exactamente eso.

En muchos casos, probablemente también debería tener varias pruebas por unidad. Las pruebas en sí mismas deberían ser simples, probando uno y solo un comportamiento en la medida de lo posible.

La noción de una "prueba unitaria" que prueba una lógica compleja, no trivial es, creo, un poco un oxímoron.

Entonces, si se ha producido ese tipo de ruptura deliberada del diseño, ¿cómo podría una prueba de unidad comenzar a producir falsos positivos de repente, a menos que la función básica de la unidad de código probada haya cambiado? Y si eso ha sucedido, es mejor que creas que hay algunos efectos no evidentes en juego. Su prueba rota, la que parece estar produciendo un falso positivo, en realidad le advierte que algún cambio ha roto un círculo más amplio de dependencias en la base del código, y debe ser examinado y reparado.

Es posible que algunas de esas unidades (muchas de ellas) deban probarse utilizando objetos simulados, pero eso no significa que tenga que escribir pruebas más complejas o elaboradas.

Volviendo a mi ejemplo ingenioso de un sistema de reservas, realmente no se pueden enviar solicitudes a una base de datos de reservas en vivo o un servicio de terceros (o incluso una instancia "dev" de ella) cada vez que prueba el código de su unidad .

Entonces usas simulacros que presentan el mismo contrato de interfaz. Las pruebas pueden validar el comportamiento de un fragmento de código relativamente pequeño y determinista. Verde en todo el tablero luego te dice que los bloques que componen tu base no están rotos.

Pero la lógica de las pruebas unitarias individuales sigue siendo lo más simple posible.

Craig
fuente
1

Por supuesto, esto es solo mi opinión, pero después de haber pasado los últimos meses aprendiendo programación funcional en fsharp (proveniente de un fondo de C #) me ha dado cuenta de algunas cosas.

Como indicó el OP, normalmente hay 2 tipos de "pruebas unitarias" que vemos día a día. Pruebas que cubren las entradas y salidas de un método, que generalmente son las más valiosas, pero son difíciles de realizar para el 80% del sistema, que se trata menos de "algoritmos" y más de "abstracciones".

El otro tipo, está probando la interactividad de abstracción, generalmente implica burlarse. En mi opinión, estas pruebas son principalmente necesarias debido al diseño de su aplicación. Al omitirlos, corre el riesgo de errores extraños y código spagetti, porque las personas no piensan en su diseño correctamente a menos que se vean obligados a hacer las pruebas primero (e incluso entonces, generalmente lo estropean). El problema no es tanto la metodología de prueba, sino el diseño subyacente del sistema. La mayoría de los sistemas construidos con lenguajes imperativos u OO tienen una dependencia inherente de los "efectos secundarios", también conocidos como "Haz esto, pero no me digas nada". Cuando confía en el efecto secundario, debe probarlo, ya que un requisito comercial u operación generalmente forma parte de él.

Cuando diseña su sistema de una manera más funcional, donde evita construir dependencias de los efectos secundarios y evita cambios de estado / seguimiento a través de la inmutabilidad, le permite concentrarse más en las pruebas de "entradas y salidas", que claramente prueban más la acción y menos cómo llegas allí. Se sorprenderá de lo que pueden ofrecerle cosas como la inmutabilidad en términos de soluciones mucho más simples para los mismos problemas, y cuando ya no dependa de los "efectos secundarios", puede hacer cosas como la programación paralelizada y asincrónica casi sin costo adicional.

Desde que comencé a codificar en Fsharp, no he necesitado un marco de imitación para nada, e incluso he eliminado mi dependencia por completo de un contenedor de COI. Mis pruebas se basan en la necesidad y el valor del negocio, y no en capas de abstracción pesadas que generalmente se necesitan para lograr la composición en la programación imperativa.

Mitchell Lee
fuente