¿Las pruebas unitarias conducen a una generalización prematura (específicamente en el contexto de C ++)?

20

Notas preliminares

No voy a entrar en la distinción de los diferentes tipos de pruebas que hay, hay ya un par de preguntas sobre estos sitios con respecto a eso.

Tomaré lo que hay allí y eso dice: prueba de unidad en el sentido de "probar la unidad aislable más pequeña de una aplicación" de la cual esta pregunta realmente deriva

El problema de aislamiento

¿Cuál es la unidad aislable más pequeña de un programa? Bueno, como yo lo veo, ¿depende de qué idioma estás codificando?

Micheal Feathers habla sobre el concepto de costura : [WEwLC, p31]

Una costura es un lugar donde puede alterar el comportamiento en su programa sin editar en ese lugar.

Y sin entrar en detalles, entiendo que una costura, en el contexto de las pruebas unitarias, es un lugar en un programa donde su "prueba" puede interactuar con su "unidad".

Ejemplos

La prueba de unidad, especialmente en C ++, requiere del código bajo prueba para agregar más costuras que se requerirían estrictamente para un problema determinado.

Ejemplo:

  • Agregar una interfaz virtual donde la implementación no virtual hubiera sido suficiente
  • División - generalización (?) - una clase (más pequeña) más "justa" para facilitar la adición de una prueba.
  • Dividir un proyecto ejecutable único en bibliotecas aparentemente "independientes", "solo" para facilitar su compilación independiente para las pruebas.

La pregunta

Probaré algunas versiones que con suerte preguntarán sobre el mismo punto:

  • Es la forma en que las Pruebas unitarias requieren que uno estructura el código de una aplicación "solo" beneficioso para las pruebas unitarias o es realmente beneficioso para la estructura de las aplicaciones.
  • ¿La generalización del código que se necesita para que sea comprobable por unidad es útil para cualquier cosa que no sean las pruebas unitarias?
  • ¿Agregar pruebas unitarias obliga a uno a generalizar innecesariamente?
  • ¿La unidad de forma prueba la fuerza en el código "siempre" también es una buena forma para el código en general como se ve desde el dominio del problema?

Recuerdo una regla general que decía que no generalices hasta que necesites / hasta que haya un segundo lugar que use el código. Con las pruebas unitarias, siempre hay un segundo lugar que usa el código, es decir, la prueba unitaria. Entonces, ¿es esta razón suficiente para generalizar?

Martin Ba
fuente
8
Un meme común es que cualquier patrón puede ser usado en exceso para convertirse en un antipatrón. Lo mismo es cierto con TDD. Se pueden agregar interfaces comprobables más allá del punto de rendimientos decrecientes, donde el código probado es menor que las interfaces de prueba generalizadas agregadas, así como en el área de costo-beneficio demasiado bajo. Un juego casual con interfaces adicionales para probar como un sistema operativo de misión en el espacio profundo podría perder completamente su ventana de mercado. Asegúrese de que la prueba agregada esté antes de esos puntos de inflexión.
hotpaw2
@ hotpaw2 Blasfemia! :)
maple_shaft

Respuestas:

23

La prueba de unidad, especialmente en C ++, requiere del código bajo prueba para agregar más costuras que se requerirían estrictamente para un problema determinado.

Solo si no considera probar una parte integral de la resolución de problemas. Para cualquier problema no trivial, debería serlo, no solo en el mundo del software.

En el mundo del hardware, esto se aprendió hace mucho tiempo, por las malas. Los fabricantes de diversos equipos han aprendido a través de siglos de innumerables puentes que caen, coches en explosión, CPUs humeantes, etc., etc., lo que estamos aprendiendo ahora en el mundo del software. Todos ellos construyen "costuras adicionales" en sus productos para hacerlos verificables. La mayoría de los automóviles nuevos hoy en día cuentan con puertos de diagnóstico para que los reparadores obtengan datos sobre lo que está sucediendo dentro del motor. Una parte importante de los transistores en cada CPU tiene fines de diagnóstico. En el mundo del hardware, cada bit de cosas "adicionales" cuesta, y cuando un producto es fabricado por millones, estos costos seguramente suman grandes sumas de dinero. Aún así, los fabricantes están dispuestos a gastar todo este dinero para la comprobabilidad.

Volviendo al mundo del software, C ++ es más difícil de probar que los lenguajes posteriores con carga de clase dinámica, reflexión, etc. Sin embargo, la mayoría de los problemas pueden al menos mitigarse. En el único proyecto C ++ en el que utilicé pruebas unitarias hasta el momento, no ejecutamos las pruebas con tanta frecuencia como lo haríamos, por ejemplo, en un proyecto Java, pero aún así formaban parte de nuestra compilación de CI, y las encontramos útiles.

¿Es la forma en que las pruebas unitarias requieren que uno estructure el código de una aplicación "solo" beneficioso para las pruebas unitarias o es realmente beneficioso para la estructura de las aplicaciones?

En mi experiencia, un diseño comprobable es beneficioso en general, no "solo" para las pruebas unitarias en sí. Estos beneficios vienen en diferentes niveles:

  • Hacer que su diseño sea comprobable lo obliga a dividir su aplicación en partes pequeñas, más o menos independientes, que solo pueden influenciarse entre sí de formas limitadas y bien definidas; esto es muy importante para la estabilidad y el mantenimiento a largo plazo de su programa. Sin esto, el código tiende a deteriorarse en código de espagueti donde cualquier cambio realizado en cualquier parte de la base de código puede causar efectos inesperados en partes distintas del programa, aparentemente no relacionadas. Lo cual, no hace falta decir, es la pesadilla de cada programador.
  • Escribir las pruebas en sí mismas en forma TDD en realidad ejercita sus API, clases y métodos, y sirve como una prueba muy efectiva para detectar si su diseño tiene sentido: si escribir pruebas en contra y la interfaz se siente incómodo o difícil, obtendrá valiosos comentarios tempranos Todavía es fácil dar forma a la API. En otras palabras, esto lo defiende de publicar sus API prematuramente.
  • El patrón de desarrollo implementado por TDD lo ayuda a enfocarse en las tareas concretas que debe hacer y lo mantiene en el objetivo, minimizando las posibilidades de que evite resolver otros problemas que no sean los que debe hacer, agregando características adicionales innecesarias y complejidad etc.
  • La rápida respuesta de las pruebas unitarias le permite ser audaz en la refactorización del código, lo que le permite adaptar y evolucionar constantemente el diseño durante la vida útil del código, evitando así la entropía del código.

Recuerdo una regla general que decía que no generalices hasta que necesites / hasta que haya un segundo lugar que use el código. Con las pruebas unitarias, siempre hay un segundo lugar que usa el código, es decir, la prueba unitaria. Entonces, ¿es esta razón suficiente para generalizar?

Si puede demostrar que su software hace exactamente lo que se supone que debe hacer, y demostrarlo de una manera suficientemente rápida, repetible, barata y determinista para satisfacer a sus clientes, sin la generalización "extra" o costuras forzadas por las pruebas unitarias, hágalo. (y háganos saber cómo lo hace, porque estoy seguro de que mucha gente en este foro estaría tan interesada como yo :-)

Por cierto, supongo que por "generalización" te refieres a cosas como la introducción de una interfaz (clase abstracta) y el polimorfismo en lugar de una sola clase concreta; de lo contrario, aclara.

Péter Török
fuente
Señor, lo saludo.
GordonM
Una nota breve pero pedante: el "puerto de diagnóstico" está principalmente allí porque los gobiernos los han ordenado como parte de un esquema de control de emisiones. En consecuencia, tiene limitaciones severas; Hay muchas cosas que potencialmente podrían diagnosticarse con este puerto que no lo son (es decir, cualquier cosa que no tenga que ver con el control de emisiones).
Robert Harvey
4

Voy a lanzar The Way of Testivus , pero para resumir:

Si está gastando una gran cantidad de tiempo y energía haciendo que su código sea más complicado para probar una sola parte del sistema, puede ser que su estructura sea incorrecta o que su enfoque de prueba sea incorrecto.

La guía más simple es esta: lo que está probando es la interfaz pública de su código en la forma en que está destinado a ser utilizado por otras partes del sistema.

Si sus pruebas se vuelven largas y complicadas, es una indicación de que usar la interfaz pública será difícil.

Si tiene que usar la herencia para permitir que su clase sea utilizada por otra cosa que no sea la instancia única para la que se usará actualmente, entonces hay una buena posibilidad de que su clase esté demasiado vinculada a su entorno de uso. ¿Puedes dar un ejemplo de una situación en la que esto sea cierto?

Sin embargo, tenga cuidado con el dogma de las pruebas unitarias. Escriba la prueba que le permita detectar el problema que hará que el cliente le grite .

deworde
fuente
Iba a agregar lo mismo: hacer una API, probar la API, desde afuera.
Christopher Mahan
2

TDD y Unit Testing, es bueno para el programa en su conjunto, y no solo para las pruebas unitarias. La razón de esto es porque es bueno para el cerebro.

Esta es una presentación sobre un marco específico de ActionScript llamado RobotLegs. Sin embargo, si hojeas las primeras 10 diapositivas más o menos, comienza a llegar a las partes buenas del cerebro.

Las pruebas de TDD y Unidad lo obligan a comportarse de una manera que sea mejor para que el cerebro procese y recuerde la información. Entonces, si bien su tarea exacta frente a usted es simplemente hacer una mejor prueba de unidad, o hacer que el código sea más comprobable por unidad ... lo que realmente hace es hacer que su código sea más legible y, por lo tanto, hacer que su código sea más fácil de mantener. Esto le permite codificar los habbits más rápido y le permite comprender su código más rápido cuando necesita agregar / eliminar funciones, corregir errores o, en general, abrir el archivo fuente.

Beto
fuente
1

probar la unidad aislable más pequeña de una aplicación

esto es cierto, pero si lo lleva demasiado lejos, no le da mucho, y cuesta mucho, y creo que es este aspecto el que promueve el uso del término BDD para que sea lo que TDD debería haber sido. a lo largo: la unidad aislable más pequeña es lo que quieres que sea.

Por ejemplo, una vez depuré una clase de red que tenía (entre otros bits) 2 métodos: 1 para configurar la dirección IP, otro para configurar el número de puerto. Naturalmente, estos eran métodos muy simples y pasarían la prueba más trivial fácilmente, pero si configura el número de puerto y luego la dirección IP, no funcionaría: el configurador de IP sobrescribía el número de puerto de forma predeterminada. Así que tuvo que probar la clase en su conjunto para garantizar un comportamiento correcto, algo que creo que falta el concepto de TDD, pero BDD le brinda. Realmente no necesita probar cada método pequeño, cuando puede probar el área más sensible y más pequeña de la aplicación general, en este caso la clase de red.

En última instancia, no hay una bala mágica para las pruebas, debe tomar decisiones sensatas sobre cuánto y a qué granularidad aplicar sus recursos de prueba limitados. El enfoque basado en herramientas que genera automáticamente trozos para usted no hace esto, es un enfoque de fuerza contundente.

Entonces, dado esto, no necesita estructurar su código de una determinada manera para lograr TDD, pero el nivel de prueba que logre dependerá de la estructura de su código, si tiene una GUI monolítica que tiene toda su lógica estrechamente vinculada a la estructura de la GUI, entonces le resultará más difícil aislar esas piezas, pero aún puede escribir una prueba de unidad donde 'unidad' se refiere a la GUI y todo el trabajo de base de datos de fondo se burla. Este es un ejemplo extremo, pero muestra que aún puede hacer pruebas automáticas en él.

Un efecto secundario de estructurar su código para que sea más fácil probar unidades más pequeñas lo ayuda a definir mejor la aplicación, y eso le permite reemplazar partes más fácilmente. También ayuda cuando se codifica, ya que será menos probable que 2 desarrolladores trabajen en el mismo componente en un momento dado, a diferencia de una aplicación monolítica que tiene dependencias entremezcladas que interrumpen el trabajo de todos los demás.

gbjbaanb
fuente
0

Has alcanzado una buena comprensión de las compensaciones en el diseño del lenguaje. Algunas de las decisiones centrales de diseño en C ++ (el mecanismo de función virtual mezclado con el mecanismo de llamada de función estática) hacen que TDD sea difícil. El lenguaje no es realmente compatible con lo que necesita para facilitarlo. Es fácil escribir C ++ que es casi imposible de realizar una prueba unitaria.

Hemos tenido mejor suerte al hacer nuestro código TDD C ++ a partir de funciones de escritura de mentalidad cuasifuncional, no de procedimientos (una función que no toma argumentos y devuelve nulo), y usar composición siempre que sea posible. Dado que es difícil sustituir estas clases miembro, nos enfocamos en probar esas clases para construir una base confiable, y luego sabemos que la unidad básica funciona cuando la agregamos a otra cosa.

La clave es el enfoque cuasifuncional. Piénselo, si todo su código C ++ fuera funciones libres que no tienen acceso a globales, eso sería una prueba rápida de unidad :)

luego
fuente