Sentido de pruebas unitarias sin TDD

28

Tenemos un nuevo (bastante grande) inicio de proyecto, que planeamos desarrollar usando TDD.

La idea de TDD falló (muchas razones comerciales y no comerciales), pero en este momento tenemos una conversación: de todos modos, ¿deberíamos escribir pruebas unitarias o no? Mi amigo dice que no tiene sentido (o casi cero) escribir pruebas unitarias sin TDD, deberíamos centrarnos solo en las pruebas de integración. Creo lo contrario, que todavía tiene sentido escribir pruebas de unidades simples, solo para hacer que el código sea más seguro para el futuro. ¿Qué piensas?

Agregado: Creo que esto no es un duplicado de >> esta pregunta << - Entiendo la diferencia entre UT y TDD. Mi pregunta no es sobre las diferencias , sino sobre el sentido de escribir pruebas unitarias sin TDD.

ex3v
fuente
22
Tengo curiosidad sobre el razonamiento que tiene tu amigo para una postura tan absurda ...
Telastyn
11
Apuesto a que la gran mayoría de los proyectos con algunas pruebas unitarias no utilizan TDD.
Casey
2
¿Cuáles serán sus niveles de integración? ¿Cuáles serán tus unidades? ¿Con qué frecuencia refactorizará por debajo de cada nivel de prueba? ¿Qué tan rápido se ejecutarán sus pruebas de integración? ¿Qué tan fáciles serán para escribir? ¿Cuántos casos combinatorios generan diferentes partes de su código? etc ... Si no conoce las respuestas a estas, entonces quizás sea demasiado pronto para tomar decisiones firmes. A veces TDD es genial. A veces los beneficios son menos claros. A veces, las pruebas unitarias son esenciales. A veces, un conjunto decente de pruebas de integración te compra casi tanto, y son mucho más flexibles ... Mantén tus opciones abiertas.
topo Reinstale a Monica el
2
Como algunos consejos prácticos de la experiencia, no pruebe todo si no está haciendo TDD. Haga una determinación sobre qué tipos de pruebas son valiosas. Descubrí que las pruebas unitarias en métodos de entrada / salida puros son extraordinariamente valiosas, mientras que las pruebas de integración en un nivel muy alto de la aplicación (por ejemplo, enviar solicitudes web en una aplicación web) también son extraordinariamente valiosas. Tenga cuidado con las pruebas de integración de nivel medio y las pruebas unitarias que requieren mucha configuración simulada. También vea este video: youtube.com/watch?v=R9FOchgTtLM .
jpmc26
Su actualización no tiene sentido con respecto a la pregunta que hizo. Si comprende la diferencia entre TDD y pruebas unitarias, entonces lo que le impide escribir pruebas unitarias. Votar para dejar su pregunta cerrada, aunque podría ver un argumento para cerrar como "poco claro sobre lo que está preguntando" en lugar de duplicado.

Respuestas:

52

TDD se utiliza principalmente (1) para garantizar la cobertura, (2) y para impulsar un diseño mantenible, comprensible y comprobable. Si no usa TDD, no obtiene cobertura de código garantizada. Pero eso de ninguna manera significa que debe abandonar ese objetivo y vivir alegremente con 0% de cobertura.

Las pruebas de regresión fueron inventadas por una razón. La razón es que, a la larga, le ahorran más tiempo en errores evitados de lo que requieren un esfuerzo adicional para escribir. Esto ha sido probado una y otra vez. Por lo tanto, a menos que esté seriamente convencido de que su organización es mucho, mucho mejor en ingeniería de software que todos los gurús que recomiendan las pruebas de regresión (o si planea caer muy pronto para que no haya un largo plazo para usted), sí, usted absolutamente debe tener pruebas unitarias, exactamente por la razón que se aplica a prácticamente cualquier otra organización en el mundo: porque detectan los errores antes que las pruebas de integración, y eso le ahorrará dinero. No escribirlos es como gastar dinero gratis en la calle.

Kilian Foth
fuente
12
"Si no usa TDD, no obtendrá cobertura de código garantizada": No lo creo. Puede desarrollarse durante dos días y durante los próximos dos días redacta las pruebas. El punto importante es que no considera una función terminada hasta que tenga la cobertura de código deseada.
Giorgio
55
@DougM - Tal vez en un mundo ideal ...
Telastyn
77
Lamentablemente, TDD va de la mano con la burla y hasta que la gente deje de hacerlo, todo lo que demuestra es que su prueba se ejecuta más rápido . TDD está muerto. Larga vida a las pruebas.
MickyD
17
TDD no garantiza la cobertura del código. Esa es una suposición peligrosa. Puede codificar contra pruebas, pasar esas pruebas, pero aún tener casos extremos.
Robert Harvey
44
@MickyDuncan No estoy muy seguro de entender completamente su preocupación. La burla es una técnica perfectamente válida utilizada para aislar un componente de los otros, de modo que las pruebas del comportamiento de ese componente se puedan realizar de forma independiente. Sí, llevado al extremo, puede conducir a un software sobre-diseñado, pero también puede hacerlo cualquier técnica de desarrollo si se usa de manera inapropiada. Además, como dice DHH en el artículo que cita, la idea de usar solo pruebas completas del sistema es igual de mala, si no peor. Es importante usar el juicio para decidir cuál es la mejor manera de probar cualquier característica en particular .
Jules
21

Tengo una anécdota relevante de algo que está sucediendo en este momento para mí. Estoy en un proyecto que no usa TDD. Nuestra gente de QA nos está moviendo en esa dirección, pero somos un equipo pequeño y ha sido un proceso largo y prolongado.

De todos modos , recientemente estaba usando una biblioteca de terceros para hacer una tarea específica. Hubo un problema con respecto al uso de esa biblioteca, por lo que se me asignó la tarea de escribir una versión de esa misma biblioteca por mi cuenta. En total, terminaron siendo aproximadamente 5,000 líneas de código ejecutable y aproximadamente 2 meses de mi tiempo. Sé que las líneas de código son una métrica pobre, pero por esta respuesta, creo que es un indicador de magnitud decente.

Necesitaba una estructura de datos particular que me permitiera hacer un seguimiento de un número arbitrario de bits. Dado que el proyecto está en Java, elegí Java BitSety lo modifiqué un poco (también necesitaba la capacidad de rastrear los 0s principales , que BitSet de Java no hace por alguna razón .....). Después de alcanzar una cobertura del ~ 93%, comencé a escribir algunas pruebas que realmente enfatizarían el sistema que había escrito. Necesitaba comparar ciertos aspectos de la funcionalidad para asegurar que serían lo suficientemente rápidos para mis requisitos finales. Como era de esperar, una de las funciones que había anulado de la BitSetinterfaz era absurdamente lenta cuando se trataba de grandes conjuntos de bits (cientos de millones de bits en este caso). Otras funciones anuladas dependían de esta función, por lo que era un enorme cuello de botella.

Lo que terminé haciendo fue ir al tablero de dibujo y encontrar una forma de manipular la estructura subyacente de BitSet, que es a long[]. Diseñé el algoritmo, les pedí a sus colegas su opinión y luego me puse a escribir el código. Luego, ejecuté las pruebas unitarias. Algunos de ellos se rompieron, y los que sí me indicaron exactamente dónde necesitaba buscar en mi algoritmo para solucionarlo. Después de corregir todos los errores de las pruebas unitarias, pude decir que la función funciona como debería. Por lo menos, podría estar tan seguro de que este nuevo algoritmo funcionó tan bien como el algoritmo anterior.

Por supuesto, esto no es a prueba de balas. Si hay un error en mi código que las pruebas unitarias no están buscando, entonces no lo sabré. Pero, por supuesto, ese mismo error podría haber estado en mi algoritmo más lento también. Sin embargo , puedo decir con un alto grado de confianza que no tengo que preocuparme por la salida incorrecta de esa función en particular. Las pruebas unitarias preexistentes me ahorraron horas, quizás días, de intentar probar el nuevo algoritmo para asegurarme de que fuera correcto.

Ese es el punto de tener pruebas unitarias independientemente de TDD, es decir, las pruebas unitarias lo harán por usted en TDD y fuera de TDD de todos modos, cuando termine refactorizando / manteniendo el código. Por supuesto, esto debe combinarse con pruebas de regresión regulares, pruebas de humo, pruebas difusas, etc., pero las pruebas unitarias , como su nombre lo indica, prueban las cosas en el nivel atómico más pequeño posible, lo que le da instrucciones sobre dónde han aparecido los errores.

En mi caso, sin las pruebas unitarias existentes, de alguna manera tendría que encontrar un método para asegurar que el algoritmo funcione todo el tiempo. Lo cual, al final ... suena mucho a pruebas unitarias , ¿no?

Shaz
fuente
7

Puede dividir el código aproximadamente en 4 categorías:

  1. Simple y rara vez cambia.
  2. Cambios simples y frecuentes.
  3. Complejo y rara vez cambia.
  4. Cambios complejos y frecuentes.

Las pruebas unitarias se vuelven más valiosas (es probable que detecten errores importantes) a medida que avanza en la lista. En mis proyectos personales, casi siempre hago TDD en la categoría 4. En la categoría 3 generalmente hago TDD a menos que las pruebas manuales sean más simples y rápidas. Por ejemplo, el código antialiasing sería complejo de escribir, pero mucho más fácil de verificar visualmente que escribir una prueba unitaria, por lo que la prueba unitaria solo me valdría la pena si ese código cambiara con frecuencia. El resto de mi código solo lo pongo bajo prueba unitaria después de encontrar un error en esa función.

A veces es difícil saber de antemano en qué categoría se ajusta un determinado bloque de código. El valor de TDD es que no se pierde accidentalmente ninguna de las pruebas unitarias complejas. El costo de TDD es todo el tiempo que pasa escribiendo las pruebas unitarias simples. Sin embargo, por lo general, las personas con experiencia en un proyecto saben con un grado razonable de certeza en qué categoría encajan las diferentes partes del código. Si no está haciendo TDD, al menos debería intentar escribir las pruebas más valiosas.

Karl Bielefeldt
fuente
1
Cuando trabaje en código como sugiere con su ejemplo de antialiasing, encuentro que lo mejor es desarrollar el código experimentalmente, luego agregar algunas pruebas de caracterización para asegurar que no rompa accidentalmente el algoritmo más tarde. Las pruebas de caracterización son muy rápidas y fáciles de desarrollar, por lo que la sobrecarga de hacerlo es muy baja.
Jules
1

Ya se trate de pruebas de unidad, componente, integración o aceptación, la parte importante es que debe automatizarse. No tener pruebas automatizadas es un error fatal para cualquier tipo de software, desde los CRUD simples hasta los cálculos más complejos. El razonamiento es que escribir las pruebas automatizadas siempre costará menos que la necesidad continua de ejecutar todas las pruebas manualmente cuando no lo haga, por órdenes de magnitud. Después de haberlos escrito, solo tiene que presionar un botón para ver si pasan o fallan. Las pruebas manuales siempre demorarán mucho tiempo en ejecutarse, y dependerán de los humanos (las criaturas vivientes que se aburren, pueden carecer de atención, etc.) para poder verificar si las pruebas pasan o no. En resumen, siempre escriba pruebas automatizadas.

Ahora, sobre la razón por la cual su colega podría estar en contra de hacer cualquier tipo de prueba unitaria sin TDD: probablemente sea porque es más difícil confiar en las pruebas escritas después del código de producción. Y si no puede confiar en sus pruebas automatizadas, no valen nada . Después del ciclo TDD, primero debe hacer que una prueba falle (por la razón correcta) para poder escribir el código de producción para que pase (y no más). Este proceso es esencialmente probar sus pruebas, por lo que puede confiar en ellas. Sin mencionar el hecho de que escribir pruebas antes del código real lo empuja a diseñar sus unidades y componentes para que sean más fácilmente comprobables (altos niveles de desacoplamiento, SRP aplicado, etc.). Aunque, por supuesto, hacer TDD requiere disciplina .

En cambio, si escribe primero todo el código de producción, cuando escriba las pruebas, esperará que pasen en la primera ejecución. Esto es muy problemático, porque puede haber creado una prueba que cubre el 100% de su código de producción, sin afirmar el comportamiento correcto (¡puede que ni siquiera realice ninguna afirmación! He visto que esto sucede ), ya que no puede ver que falla primero para verificar si está fallando por la razón correcta. Por lo tanto, puede tener falsos positivos. Los falsos positivos finalmente romperán la confianza en su conjunto de pruebas, forzando esencialmente a las personas a recurrir a las pruebas manuales nuevamente, por lo que tiene el costo de ambos procesos (pruebas de escritura + pruebas manuales).

Esto significa que debe encontrar otra forma de evaluar sus pruebas , como lo hace TDD. Por lo tanto, recurre a la depuración, comentando partes del código de producción, etc., para poder confiar en las pruebas. El problema es que el proceso de "probar sus pruebas" es mucho más lento de esta manera. Agregar este tiempo al tiempo que pasará ejecutando pruebas ad-hoc manualmente (porque no tiene pruebas automáticas mientras codifica el código de producción), en mi experiencia, resulta en un proceso general que es mucho más lento que practicar TDD "por el libro" (Kent Beck - TDD por ejemplo). Además, estoy dispuesto a hacer una apuesta aquí y decir que realmente "probar sus pruebas" después de que fueron escritas requiere mucha más disciplina que TDD.

Entonces, tal vez su equipo pueda reconsiderar las "razones comerciales y no comerciales" para no hacer TDD. En mi experiencia, la gente tiende a pensar que TDD es más lento en comparación con solo escribir pruebas unitarias una vez que se hace el código. Esta suposición es errónea, como has leído anteriormente.

MichelHenrich
fuente
0

A menudo, la calidad de las pruebas depende de su procedencia. Soy regularmente culpable de no hacer TDD 'real': escribo un código para demostrar que la estrategia que me gustaría usar realmente funciona, luego cubro cada caso que el código debe respaldar con las pruebas posteriores. Por lo general, la unidad de código es una clase, para darle una idea general de cuánto trabajo haré felizmente sin la cobertura de la prueba, es decir, no una gran cantidad. Lo que esto significa es que el significado semántico de las pruebas coincide muy bien con el sistema bajo prueba en su estado 'terminado', porque las escribí sabiendo qué casos cumple el SUT y cómo los cumple.

Por el contrario, TDD con su política de refactorización agresiva tiende a realizar pruebas obsoletas al menos tan rápido como puede escribirlas, ya que la interfaz pública del sistema bajo los cambios de prueba. Personalmente, considero que la carga de trabajo mental de diseñar las unidades funcionales de la aplicación y mantener la semántica de las pruebas que la cubren sincronizadas es demasiado alta para mantener mi concentración y el mantenimiento de la prueba con frecuencia se cae. La base de código termina con pruebas que no prueban nada de valor o simplemente están equivocadas. Si tiene la disciplina y la capacidad mental para mantener actualizado el conjunto de pruebas, practique TDD tan rigurosamente como desee. No lo hago, así que lo he encontrado menos exitoso por esa razón.

Tom W
fuente
0

En realidad, el tío Bob mencionó un punto muy interesante en uno de sus videos de Clean Coders. Dijo que el ciclo Rojo-Verde-Refactor se puede aplicar de 2 maneras.

Primero es la forma convencional de TDD. Escriba una prueba reprobatoria, luego pase la prueba y finalmente refactorice.

La segunda forma es escribir un código de producción muy pequeño e inmediatamente seguirlo con su prueba unitaria y luego refactorizarlo.

La idea es ir en pasos muy pequeños . Por supuesto, pierde la verificación del código de producción de que su prueba pasó de rojo a verde, pero en algunos casos donde estaba trabajando principalmente con desarrolladores junior que se negaron incluso a tratar de comprender TDD, resultó ser algo efectivo.

Nuevamente repito (y esto fue enfatizado por el tío Bob) la idea es ir en pasos muy pequeños y probar de inmediato el código de producción que se acaba de agregar.

Songo
fuente
"... la idea es ir en pasos muy pequeños e inmediatamente probar el código de producción que se acaba de agregar": No estoy de acuerdo. Lo que describe es bueno cuando ya tiene una idea clara de lo que quiere hacer y quiere trabajar en los detalles, pero primero necesita tener una idea general. De lo contrario, siguiendo pasos muy pequeños (prueba, desarrollo, prueba, desarrollo) puede perderse en los detalles. "Si no sabe a dónde va, es posible que no llegue allí".
Giorgio