Soy un fiel defensor de hacer tests y sobre todo de hacerlos antes de escribir una linea de código, pero no todo lo que hacemos tiene que estar hecho con TDD. Quizás el título sea un poco clickbait,.. en este post me gustaría que reflexionásemos sobre algunos aspectos a tener en cuenta de por qué Test Driven Development no es una bala de plata aunque ayuda mucho al diseño y de algunos temas interesantes alrededor del testing, refactoring y software.
"Cuando la única herramienta que tienes es un martillo, todo problema comienza a parecerse a un clavo".
No todo lo que hacemos tiene que estar hecho con TDD. ¡Pensar antes de escribir es mucho más importante! Por esta razón, estamos seguro de que es posible hacer buen software sin TDD. Aunque creo que hacer software sin TDD es más complicado porque no todo el mundo es capaz de pensar antes de escribir. Además a veces las «circunstancias» nos empujan a ir rápido, a tenerlo listo ya, o incluso a pensar (ilusos de nosotros) esto lo arreglamos más adelante. Del mismo modo, podemos llegar a un lugar oscuro si no nos planteamos a probar el software lo más pronto posible. Seguro que alguna vez nos ha pasado que llegamos a un una clase, servicio o feature que es difícil de probar. ¡eso sin duda es un «mal olor»! Quizás en el futuro ese mal olor acabe en algo podrido que y nos dé algún que otro dolor de cabeza. En cualquier caso cuando usamos TDD este nos provee de un proceso para analizar los problemas, para comprender qué es lo que está pasando, qué casos se nos pueden presentar, qué casos son excepcionales,….. además nos proporciona un arnés de seguridad para ir mejorando(refactor). Sin este proceso de TDD: hacer un test, comprobar que falla, hacer código para pasar el test y refactorizar, Lo qué tenemos es un proceso custom, donde cada miembro del equipo crea de manera distinta y por quizás lleguemos a situaciones en las que modificar un código se nos haga un poco cuesta arriba.
Por otro lado en TDD tenemos el paso de refactoring. Hacer refactoring es cambiar el código interno sin cambiar su API, con el objetivo de hacerlo más entendible/extensible/rápido,… pero ¿es posible hacerlo sin test? Claro que sí, hoy en día los IDE como PHPStorm nos proveen de un montón de cambios automáticos, lo bastantes seguros como para poder mejorar nuestro código. Eso sí, por muy sencillo que nos parezca el cambio siempre puede fallar… y que se nos quiten las ganas de volver a renombrar esa variable «priceTax» con un nombre mejor. En mi opinión, si estás cambiando el código sin tener un arnés de seguridad (o sin pensamiento de crear uno) o estás creando una feature y a la mitad con 5 ficheros modificados crees que lo mejor es renombrar también esa variable… mejor no lo llamemos refactoring, llámalo cowbow style.
En el tema del refactoring nos encontramos con algunas lagunas, alguna vez haciendo Mob o Pair programming estábamos en la refactorización de algo y pensamos ¿por qué no abstraemos esto a una nueva clase? Y es ahí cuando llegan las dudas… ¿Tengo que hacer un test para la nueva clase y luego moverlo todo? No, teniendo los test en verde, se puede hacer lo que sea (menos añadir funcionalidad) siempre que se mantengan los test en verde. El objetivo de los tests es darnos libertad para cambiar el código y encontrar las abstracciones correctas para hacer que el código sea más sencillo de entender y de cambiar. En resumen, una unidad no tiene porqué ser una clase y hacer una cosa cada vez.
Y llegamos al siguiente punto que podríamos comentar referente a los tes, nunca confíes en un test que no has visto fallar. Y es que muchas veces nos encontramos con tests que no van a fallar porque el escenario no está preparado o porque ya se cubrió ese mismo escenario en un test anterior. Quizás alguien creó ese test con el objetivo de que podamos subir la cobertura de código, o se no se hizo refactoring de los tests… Aunque llevemos mucho tiempo haciendo TDD no debemos de confiarnos y hay que comprobar que el test falla antes de seguir.
Al hilo del párrafo anterior, no solo tenemos que ver que el test falla, también las aserciones tienen que sernos útiles. A veces cuando ejecutamos un test vemos:false is not equal to true
¿qué hacemos con esto? Si seguimos el flujo de TDD, es necesario que seamos pacientes y que leamos los mensajes de error. Si leemos un mensaje como el anterior, quizás sea necesario que demos un paso atrás, analizar qué es lo que estamos probando, mejorar esa aserción y así no avergonzarnos de ella en el futuro.
Esto último no quiere decir que tengamos que llenar nuestros test de aserciones. Si tenemos muchas aserciones quizás es que no tenemos claro qué es lo que estamos probando o que estamos comprobando demasiadas aspectos en el mismo test. Pero ¿por qué llegamos al punto de tener varias aserciones? Quizás sea porque para «montar» el tests es necesario demasiado andamiaje, porque la clase que necesitamos probar tiene muchos colaboradores y por ello somos reacios a añadir un nuevo test y preferimos hacerlo todo en el mismo. Llegados a este punto quizás lo mejor que podemos hacer es «dar un paso atrás» y repensar un poco el diseño de esa clase. En ocasiones es bueno que tengamos una medida cuantitativa como: no más de 2 assert por test. Del mismo modo si el tiempo de setup es un impedimento (en pruebas de aceptación) deberemos sopesar los pros y contras de testear desde tan afuera, pero esto podemos dejarlo para otro post.
Otro tema que podemos comentar acerca de TDD es que si tenemos claro nuestro diseño no necesitamos TDD. TDD nos provee una respuesta rápida sobre cómo es nuestro diseño. Como estuvimos comentando antes: si algo es difícil de testear, quizás sea difícil de mejorar y por tanto debamos darle una vuelta al diseño. ¿Hemos visto algún test con 20, 50 lineas de configuración antes ni siquiera de instanciar la clase que vamos a testear?¿los mocks son complicados?¿De verdad son necesarios tantos colaboradores? ¿Tienes una interfaz con 5 o 7 métodos? ¿Estas haciendo assert sobre los mocks (se ha llamado 1 vez a X y 2 veces a Y)? Ese es el feedback que nos provee TDD. Al final de lo que se trata es de escuchar a nuestro código y para esto quizás no haga falta TDD, pero sí que es cierto que TDD nos ayuda a hacernos estas preguntas y otras muchas pronto y repetidamente.
Siguiendo con el tema del diseño, en TDD además de cuidar el diseño del código y los colaboradores también tenemos que echarle un vistazo a los tests y darles cariño al igual que al código. Por ejemplo a las tablas(aka dataproviders), son geniales y nos ayudan a tener probar varios casos casi sin esfuerzo pero pueden ser complicados de leer y entender cuando algo falla.
Y por último pero no menos importante, tenemos la encapsulación. ¿por qué necesitamos todos esos métodos públicos? ¿son solo para los tests? Si estamos haciendo un test y nos vemos en la necesidad de convertir a público uno de los métodos… ¿de verdad es necesario hacer es solo para un test? Quizás tengamos que volver al diseño y darle una vuelta a este test, porque si dejamos ese método público puede que se acabe llamando desde otro punto. De lo que se trata con esto de TDD es de obtener feedback y pararnos a pensar antes de seguir.
Para terminar, «simple is not easy» uno de las preceptos que tenemos que tener en la cabeza es que escribir software no es fácil. Es decir, añadir un nuevo campo a una tabla es fácil, pero puede hacer que las cosas no sean tan simples (recuerda todos tenemos en mente esa tabla que tiene más de 15 columnas)
Conclusiones
Este post es una serie de reflexiones acerca de pararnos a pensar antes de escribir y escuchar a los tests. En mi opinión, el objetivo principal de TDD es proporcionarnos feedback sobre nuestro diseño. Así que lo repetiremos una vez más: escucha a tus sus tests, ellos te están hablando de tu diseño. Eso no quiere decir que haciendo TDD tengamos la vida resuelta, ni mucho menos, si somos hay otros factores inherentes al software y a las personas que desarrollamos que hacen esto de escribir código algo complejo.
El resumen sería: el proceso TDD es conceptualmente simple de seguir, pero a medida que lo hacemos, encontraremos que desafía nuestras habilidades de diseño. TDD no es difícil, diseñar e implementar software si que lo es. Así que lo mejor que podemos es no rendirnos, seguir trabajando y escuchando a nuestros tests.
En mi experiencia la realidad es otra donde hay montones de factores. TDD si genial ok pero si encontramos una planificación perfecta donde el proyecto está planificado a nivel negocio y arquitectura de Soft.
El tema es que alguien que sabe como hacer TDD sin que sea una carga y un problema ya sabe como tiene que definir su arquitectura estrategia por lo que no le hace el «escucha tus test que te dirán de tu estrategia» , y en parte tiene razón pero es una paradoja… Si no sabes como definir la estrategia correcta no sabes escribir los test y viceversa.
Hay que añadir el contexto. Que muchos libros de testing, o patrones de testing ( típico cono de helado) se basan en casos de gente que hay programado para sectores como automovilismo. SI ok testing ahí vale un método que hace una operación matemática compleja ..bien. pero en Web Apps. la mayoría de los métodos son extremadamente simples. Donde los test de aceptación ya no solo no es como antes que tener una infra era un horror. Si no que ese test te hace un coverage muy amplio y levantar una infra con k8s similar a prod es extremadamente rápido y si no es que lo estás haciendo mal para cumplir el servicio. Así que escucha tus test de aceptación que te dirán también.
Resumiendo que hay que razonar que es lo que proporciona más valor al proyecto. Y como se ha comentado muy correctamente. Para y piensa antes antes de nada.
Me gustaMe gusta
Gracias por aportar. Creo que el contexto es una de los inputs más importantes cuando estamos desarrollando: no es lo mismo un sitio con estrés porque hay que facturar, que otro sitio más tranquilo donde tengamos un poco más de «espacio». A lo que comentas de los test de aceptación, me encanta la aproximación que se hace en Growing Object-Oriented Software, Guided by Tests (https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627) donde se hace un enfoque outside-in dejando los test de aceptación «fallando» para no perder el foco mientras se van adentrando el capas más internas
Me gustaMe gusta
por aclarar decir que me parece muy interesante y acertado el post, ya que releyendo parece que no estoy de acuerdo. Y comento algo similar con otros contextos y experiencias.
Me gustaMe gusta