Recientemente di una charla sobre depuración para la comunidad Java de Londres. Durante la parte de preguntas y respuestas de la charla, alguien me preguntó sobre mi enfoque del desarrollo basado en pruebas. En el pasado, miré esa práctica bajo una luz más positiva. Escribiendo muchas pruebas. ¿Cómo puede ser eso malo?
Pero a medida que pasó el tiempo, lo veo bajo una luz diferente.
Lo veo como una herramienta muy limitada que tiene casos de uso muy específicos. No encaja en el tipo de proyectos que construyo y, a menudo, dificulta los procesos fluidos que se supone que debe promover. Pero retrocedamos por un segundo. Me gustó mucho este post que separa los tipos y problemas en TDD. Pero simplifiquemos un poco, aclaremos que todo PR debe tener una buena cobertura. Esto no es TDD. Es solo una buena programación.
TDD es más que eso. En él, necesitamos definir las restricciones y luego resolver el problema. ¿Es ese enfoque superior a resolver el problema y luego verificar que las restricciones sean correctas? Esa es la premisa central de TDD frente a solo escribir una buena cobertura de prueba.
TDD es un enfoque interesante. Es especialmente útil cuando se trabaja con lenguajes poco escritos. En esas situaciones, TDD es maravilloso, ya que cumple el papel de un compilador y un filtro estrictos.
Hay otros casos en los que tiene sentido. Cuando estamos construyendo un sistema que tiene entradas y salidas muy bien definidas. Me he encontrado con muchos de estos casos al crear cursos y materiales. Cuando se trabaja con datos del mundo real, esto sucede a veces cuando tenemos un middleware que procesa datos y los genera en un formato predefinido.
La idea es construir la ecuación con las variables ocultas en el medio. Entonces la codificación se convierte en llenar la ecuación. Es muy conveniente en casos como ese. Codificar se convierte en llenar los espacios en blanco.
“El desarrollo basado en pruebas ES contabilidad de doble entrada. Misma disciplina. Mismo razonamiento. Mismo resultado.” – tío Bob Martín
Yo diría que las pruebas son un poco como la contabilidad de doble entrada. Sí. Deberíamos tener pruebas. La pregunta es ¿debemos construir nuestro código basado en nuestras pruebas o viceversa? Aquí la respuesta no es tan simple.
Si tenemos un sistema preexistente con pruebas, entonces TDD tiene todo el sentido del mundo. Pero probando un sistema que aún no estaba construido. Hay algunos casos en los que tiene sentido, pero no tan a menudo como uno pensaría.
El gran reclamo de TDD es “su diseño”. Las pruebas son efectivamente el diseño del sistema, y luego implementamos ese diseño. El problema con esto es que tampoco podemos depurar un diseño. En el pasado, trabajé en un proyecto para una importante empresa japonesa. Esta empresa tenía uno de los conjuntos de libros de diseño anexos más grandes y detallados. Basándose en estas especificaciones de diseño, la empresa construyó miles de pruebas. Se suponía que íbamos a pasar una gran cantidad de pruebas con nuestro sistema. Tenga en cuenta que la mayoría ni siquiera eran automáticos.
Las pruebas tenían errores. Hubo muchas implementaciones en competencia, pero ninguna de ellas encontró los errores en las pruebas. ¿Por qué? Todos usaron el mismo código fuente de implementación de referencia. Fuimos el primer equipo en omitir eso e implementar una sala limpia. Perpetuó estos errores en el código, algunos de ellos eran errores graves de rendimiento que afectaron a todas las versiones anteriores.
Pero el verdadero problema fue el lento progreso. La empresa no podía avanzar rápidamente. Los defensores de TDD se apresurarán a comentar que un proyecto de TDD es más fácil de refactorizar ya que las pruebas nos dan una garantía de que no tendremos regresiones. Pero esto se aplica a proyectos con pruebas realizadas después del hecho.
TDD se centra en gran medida en las pruebas unitarias rápidas. No es práctico ejecutar pruebas de integración lentas o pruebas de ejecución prolongada que pueden ejecutarse durante la noche en un sistema TDD. ¿Cómo verifica la escala y la integración en un sistema importante?
En un mundo ideal, todo simplemente encajará en su lugar como legos. No vivo en un mundo así, las pruebas de integración fallan gravemente. Estas son las peores fallas con los errores más difíciles de rastrear. Prefiero tener una falla en las pruebas unitarias, por eso las tengo. Son fáciles de arreglar. Pero incluso con una cobertura perfecta, no prueban la interconexión correctamente. Necesitamos pruebas de integración y encuentran los errores más terribles.
Como resultado, TDD enfatiza demasiado las pruebas unitarias "agradables de tener", sobre las pruebas de integración esenciales. Sí, deberías tener ambos. Pero debo tener las pruebas de integración. Esos no encajan tan claramente en el proceso TDD.
Escribo probando la forma que elijo caso por caso. Si tengo un caso en el que la prueba por adelantado es natural, lo usaré. Pero para la mayoría de los casos, escribir el código primero me parece más natural. Revisar los números de cobertura es muy útil al escribir pruebas y esto es algo que hago después del hecho.
Como mencioné antes, solo compruebo la cobertura para las pruebas de integración. Me gustan las pruebas unitarias y monitorear la cobertura allí, ya que también quiero una buena cobertura allí. Pero para la calidad, solo importan las pruebas de integración. Un PR necesita pruebas unitarias, no me importa si las escribimos antes de la implementación. Deberíamos juzgar los resultados.
Cuando Tesla estaba construyendo sus fábricas Model 3, entró en un infierno de producción. La fuente de los problemas fue su intento de automatizar todo. El Principio de Pareto se aplica perfectamente a la automatización. Algunas cosas son muy resistentes a la automatización y hacen que todo el proceso sea mucho peor.
Un punto en el que esto realmente falla es en las pruebas de interfaz de usuario. Soluciones como Selenium, etc. lograron grandes avances en las pruebas de front-end web. Aún así, la complejidad es tremenda y las pruebas son muy frágiles. Terminamos con pruebas difíciles de mantener. Peor aún, encontramos que la interfaz de usuario es más difícil de refactorizar porque no queremos reescribir las pruebas.
Probablemente podamos cruzar el 80 % de la funcionalidad probada, pero hay un punto de rendimiento decreciente para la automatización. En esos entornos, TDD es problemático. La funcionalidad es fácil pero construir las pruebas se vuelve insostenible.
No estoy en contra de TDD pero no lo recomiendo y efectivamente no lo uso. Cuando tenga sentido comenzar con una prueba, podría hacerlo, pero eso no es realmente TDD. Juzgo el código basado en los resultados. TDD puede proporcionar excelentes resultados, pero a menudo enfatiza demasiado las pruebas unitarias. Las pruebas de integración son más importantes para la calidad a largo plazo.
La automatización es genial. Hasta que se detiene. Hay un punto en el que las pruebas automatizadas tienen poco sentido. Nos ahorraría mucho tiempo y esfuerzo aceptar eso y enfocar nuestros esfuerzos en una dirección productiva.
Esto se debe a mi preferencia como desarrollador de Java al que le gustan los lenguajes estrictos y con seguridad de tipo. Los lenguajes como JavaScript y Python pueden beneficiarse de un mayor volumen de pruebas debido a su flexibilidad. Por lo tanto, TDD tiene más sentido en esos entornos.
En resumen, la prueba es buena. Sin embargo, TDD no hace mejores pruebas. Es un enfoque interesante si te funciona. En algunos casos es enorme. Pero la idea de que TDD es esencial o incluso que mejorará significativamente el código resultante no tiene sentido.
También publicado aquí.