Charlaba la semana pasada en Twitter sobre TDD y, en concreto, el concepto de triangulación a raíz de este post en el que se menciona lo siguiente:
A esta altura, ya es evidente que sería más facil escribir la implementación obvia que seguir haciendo ramas de decisión.
Antes esto, parece razonable preguntarse:
¿Tiene sentido utilizar TDD cuando existe una implementación obvia?
Y, ojo que esto es importante, hablo de utilizar TDD, no de tener tests. Hablo de la metodología de desarrolo descrita en el Test Driven Development auténtico. Si nos centramos en tests, ya he explicado varias veces lo que considero que se debe cubrir con tests y he puesto ejemplos de cómo testear determinados escenarios.
En este post quiero divagar un poco sobre cuándo tiene sentido aplicar TDD.
OJO: Si eres un fanático convencido de TDD, probablemente no te guste el post, y me temo que basarás tu argumentación en que no sé aplicar TDD. Es posible, pero lo cierto es que lo he aplicado con relativo éxito durante el tiempo suficiente como para poder formarme una opinión al respecto. Por supuesto, puedo estar equivocado y te agradecería que me explicaras por qué.
Me gustaría poder dar unas pautas universales de cuándo usar TDD y cuándo no merece la pena, pero lo cierto es que es un caso similar a elegir un lenguaje de programación e influyen tantos factores que no me siento capacitado para dar una serie de reglas categóricas sobre el tema.
Lo que sí puedo hacer, y creo que puede resultar útil, es analizar distintos aspectos de TDD y comentar mi experiencia personal con ellos. Tómatelo como lo que es, una opinión y no una verdad absoluta, y si te apetece comentar cómo te ha ido a ti con esta historia, estupendo, seguro que más de uno te lo agradecemos.
Por cierto, para los que no tienen ganas de leer más, no, no creo que merezca la pena aplicar TDD si existe una implementación obvia, aunque haya quien lo piense (y no, esta vez no ha sido Robert C. Martin, quien directamente aboga por lo contrario).
TDD como herramienta de diseño
Es uno de los argumentos más utilizados por los defensores y practicantes de TDD. De hecho, para muchos la última D de TDD no es development, sino design. Ciertamente, TDD nos puede ayudar en el diseño, pero quizá no tanto como nos gustaría.
Por una parte nos obliga a escribir código testeable, lo que se supone que es bueno (y es algo que ni siquiera voy a cuestionar ahora).
Sin embargo, también hay que recordar que lo que nos obliga a escribir es código testeable con tests unitarios, y ahí entramos en problemas semánticos sobre lo que es un test unitario. Si al final esto se va a reducir a crear un montón de header interfaces para escribir tests de interacción de esos que no validan gran cosa, no sé si realmente tenemos un diseño mejor o sólo estamos sobrecomplicando el diseño para poder escribir unos tests que no aportan valor y sólo sirven para que las métricas queden bonitas.
Por otra, es cuestionable que TDD nos lleve a diseñar un algoritmo de la mejor manera. Os recomiendo echar un vistazo a esta antiquísima discusión sobre diseñar un algoritmo para resolver sudokus usando TDD. Merece la pena ver las dos aproximaciones, con y sin TDD.
En su favor, debo decir que aplicar TDD nos ayuda a diseñar el API que tendrá nuestro componente. Al escribir primero el código cliente (los tests, en este caso) nos obliga a pensar cómo queremos utilizarlo y nos guía para diseñar un API que tenga sentido.
En ese sentido podríamos decir que TDD es una buena herramienta para diseñar APIs pero… La verdad es que no es necesario para eso y, a veces, directamente nos lleva a diseñar un API muy adecuada para los tests pero no necesariamente apta para el código de producción. Si queremos realmente diseñar correctamente el API de un componente, es mejor aplicar directamente diseño top-down y que sean los clientes reales de nuestro componente los que marquen el API (que luego podremos implementar o no con TDD).
En definitiva, creo que nunca he sido capaz de sacarle excesivo partido a TDD como herramienta de diseño. Además, hay aspectos del diseño de un componente que son complicados que surjan naturalmente aplicando TDD, como la transaccionalidad o la eficiencia.
TDD como forma de hacer testing
Los que realmente usan TDD no suelen hacer mucho enfásis en esto y consideran los tests como un producto residual (aunque útil e importante) de la técnica.
Es decir, tú no haces TDD por conseguir tests, sin por conseguir código mejor. Si eso además deja un rastro de tests que luego puede seguir usando como documentación o para que verificar que el código funciona después de futuras refactorizaciones, mejor para ti, pero insisto, no es el objetivo fundamental. Incluso hay gente que, una vez ha llegado a la solución, borra los tests, o al menos parte de ellos, porque considera que no aportan ya nada.
Reconozco que al principio cuando usaba TDD lo hacía un poco con la mentalidad de olibgarme a escribir tests. Y además con muy buena cobertura. A fin de cuentas, no escribes código de producción si no hay antes un test fallando, por lo que se supone que la cobertura debería ser muy cercana al 100%.
Eso me llevó a algunos problemas de lo que mencionaba antes, con muchos tests de dudoso valor y, lo que es peor, con código que había sido desarrollado siguiendo religiosamente TDD y estaba completamente testado, pero no sólo no tenía una buena implementación, sino que ni siquiera funcionaba en producción o lo hacía de forma deficiente.
Aquí admito sin duda mi parte de culpa, y estoy seguro de que hay gente a la que no le pasa eso, pero la verdad es que he visto bastantes casos reales de este problema. Aplicar TDD no es sencillo, y es fácil acabar con la sensación de que para que realmente funcione TDD en el fondo hay que hacer “trampas” y saber desde el principio donde vas a llegar. Si no, puedes tener todos los tests que quieras, que luego en producción ya veremos lo que pasa.
Si lo que estás buscando es conseguir tener una buena suite de tests, que cumpla con el objetivo fundamental de los tests: conseguir confianza, dudo que TDD sea la mejor forma de hacerlo.
Aunque sólo seas por un problema muy básico: TDD se limita a hablar de tests unitarios y para tener una buena suite de tests hay que jugar con todos los tipos de tests que tienes a tu disposición, incluso con los tipos de tests más exóticos.
Diseñar una estrategia de testing requiere un enfoque más integral, pensando qué quieres conseguir, qué partes son más importantes testear, en qué partes es más fácil cometer errores, y qué tipo de tests te pueden ayudar a cubrir tus objetivos. Es eso lo que debe marcar el aspecto de una suite de tests, más que aplicar TDD o no.
TDD como herramienta de exploración
Otro de los argumentos a favor de usar TDD es que te permite explorar un problema de forma incremental hasta alcanzar una solución. La idea es que al ir construyendo los tests poquito a poquito (con baby steps, si quieres sonar más guay), puedes ir conociendo cada vez más cosas del problema que intentas resolvar sin tener que intentar abarcarlo todo de golpe.
La verdad es que en ese sentido sí que me resulta útil TDD, especialmente cuando no tengo ni idea de cómo afrontar algo. Si no sabes por dónde empezar, empezar por algo, lo que sea, aunque luego lo borres todo y empieces otra vez de cero, es mejor que nada. Empezar a escribir código suele ayudar también a tener cierta sensación de avance (aunque sea fictia) que evita la parálisis por análisis en la que a veces nos sumimos.
Normalmente no suelo empezar por aplicar TDD y soy de los que prefiere pensar un rato antes, pero hay casos en los que estoy bloqueado y aplicar TDD me permite avanzar algo. No suelo dar pasos taaaaan pequeños como para saltar de implementación trivial en implementación trivial, pero si me obligo a resolver facetas concretas del problema en lugar de atacarlo todo a la vez.
Puede sonar raro, pero uno de los escenarios en los que suelo aplicar TDD es al escribir consultas SQL complejas. Probablemente porque para los que desarrollamos las típicas aplicaciones aburridas de línea de negocio sean una de las cosas más complicadas que llegamos a hacer, y además suelen ser muy costosas de testear a mano porque están llenas de casos extraños que no siempre son fáciles de simular.
Por supuesto, TDD no es la única herramienta de exploración que existe y tampoco es la única que utilizo. Muchas veces me gusta montar un proyecto de pruebas (un spike para los amantes del postureo) y empezar a jugar para investigar algo. Si el lenguaje se presta (y cada vez trato más de usar lenguajes que se presten porque me resulta muy productivo), disponer de un buen REPL es un excelente complemento a TDD como cuenta Manuel Rivero en su muy recomendable blog.
Conclusiones
Puede sonar todo un poco negativo hacia TDD, y más viniendo de alguien que hace 4 años defendía TDD y recomendaba no escribir todos los tests unitarios a la vez para seguir TDD, pero lo cierto es que todo evoluciona y no es la primera (y seguro que tampoco la última) vez que escribo posts con cierto tono contradictorio. Prefiero ser sincero que dogmático.
Estoy convencido de que me dejo muchos aspectos importantes de TDD y de que parte de los motivos por lo que no obtengo más beneficios de aplicarla es culpa mía, pero la realidad es que, simplificando mucho, para mi el principal punto a favor de usar TDD es la facilidad para explorar un problema en lenguajes en los que no es sencillo usar un REPL.
Eso no quiere decir que no le dé valor a los tests. Al contrario. Creo que es algo a lo que cada vez le he ido dado más valor, y quizá justo por eso cada vez los he alejado más de TDD. Los tests son lo bastante importantes como para diseñarlos de forma independiente y no como un subproducto de una metodología de desarrollo.
Posts relacionados: