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.
Gran post como siempre,
a mi la verdad es que me encanta hacer TDD, soy un poco el mas fan de la oficina. Pero tampoco me obsesiona que los otros lo hagan. Lo unico que me obsesiona es que el codigo este testeado, para evitar problemas a futuro.
Lo que mas me gusta del TDD, es que te obliga a pensar, que es algo que a veces no hacemos tanto como deberiamos. Asi que si empiezas por los tests, tienes que pensar en las situaciones que se van a dar, sino vas un poco a lo loco.
Sobre el debate de diseño, si que es cierto que TDD no es lo mejor. Normalmente el codigo que sale de las primeras iteraciones es un poco a saco, basandose en el principio de que solo tenga que pasar el test y listos. Pero mucha gente olvida que el tercer paso de cada iteracion es refactorizar, y quizas es el paso mas importante.
Y al final refactorizar con tests es mucho mas facil o al menos mucho mas seguro.
Pero bueno este es el debate sin fin. Si alguien con poca idea de TDD me pregunta, le recomendare que haga varias katas, para que juegue y luego decida. Pero si alguien lo ha probado y no le gusta, pues perfecto.
Eso si, que no venga el tipico listo que diga que hacer tests es perder el tiempo. Que de esos hay unos cuantos, pero no se les suele ver en blogs ni en charlas, estan demasiado ocupando parcheando sus bugs.
Gracias por el post. Debería comentarlo con más detalle, pero lo primero que comentaría es que, por lo menos en mi práctica de TDD (desde hace 8 años prácticamente diaria) no me veo obligado a escribir test unitarios. Cuando uno se quita de encima esa mochila («tengo que escribir test unitarios unitarios UNITARIOS»), sino tests que prueben lo que estoy escribiendo, pasando de rojo a verde a refactor, todo fluye de mejor forma. Y sí, para mí TDD me ayuda en el diseño: me ha permitido hasta cambiar en algún momento de diseño interno sin grandes dificultades. Y lo que siempre trato es de empujar por la simplicidad. Al escribir los pequeños usos del software en construcción, trato de encontrar la solución más simple para que el test pase a verde. Eso me ha evitado arquitecturas de astronautas, y complicaciones innecesarias. El cubrimiento de código es sólo una métrica, y no un objetivo en sí, en mi caso.
Gracias por los comentarios a ambos.
Hay una parte que mencionáis los dos: la facilidad para mejorar el diseño gracias a tener tests.
En eso estoy totalmente de acuerdo, tener tests te permite hacer cambios con mayor seguridad y eso a su vez permite iterar hasta conseguir un diseño mejor, pero en realidad para eso necesitas tests, no hace falta usar TDD.
Puedes conseguir el mismo efecto escribiendo todos los tests a posteriori. Aquí cabría argumentar si la calidad de los tests creados con TDD es mejor que la de los tests creados sin TDD, pero eso es abrir otra línea de discusión ;-)
Como nos tienes acostumbrados, gran post. Muchas gracias por tomarte tu tiempo para escribirlo, de mayor me gustaría ser como tú ;)
Primero una pequeña aportación que a mi me funciona:
Lo que comentas respecto a cómo afecta al diseño, yo he notado una diferencia muy grande entre cómo afecta a mi diseño cuando uso una aproximación outside-in frente a inside-out. Personalmente me quedo con la primera la mayoría de las veces porque me aproxima lo máximo posible al consumidor de mi código desde el primer momento.
Además de usar con más frecuencia outside-in, intento evitar verificar la interacción y prefiero irme más a que sean test funcionales. Cuando lo que quiero probar tiene varios colaboradores y únicamente quiero probar una parte aislada de la funcionalidad, hago un stub del colaborador, se lo inyecto y listo. Pero tampoco me tiembla el pulso por construir el colaborador en un constructor y que se ejecute ese código también.
Hay veces que uso la opción mixta respecto a los colaboradores. Permito la inyección pero admito que sea nula. Si me viene a nula, la creo en el constructor, pero si me viene uso esa. Eso me facilita la vida en los tests a la vez que no me la quita en su uso habitual. Por supuesto, esto no lo hago siempre, sólo en algunos tipos de colaboradores.
Y ahora la parte «rant», no contra tí, pero si contra un sector fundamentalista:
Me ha encantado cuando has mencionado precisamente el «TDD auténtico». Yo sueño usar la coletilla «de libro» y es algo que uso con frecuencia porque me cansa mucho tanto purismo. Y no me refiero en exclusiva al purismo en TDD, me lo encuentro en DDD, BDD, Agile y cualquier otra cosa que tenga un nombre y un credo que sólo entiende una forma de hacer las cosas.
La respuesta habitual es «pero es que entonces no es TDD, es otra cosa distinta». No voy a entrar ahora en un debate de si lo que hago en mi casa los domingos es paella o un guiso de arroz, tengo claro que lo que no pienso hacer es tener que soltar un monólogo.
Pero donde realmente le veo el fallo al fundamentalismo es cuando entramos en cosas como:
– Yo desarrollo los tests primeros con un flujo de rojo, verde y refactor, pero el refactor no lo hago siempre después de cada test y a veces me aguanto algunos tests para ver cómo voy enfocando el diseño.
– Lo que para unos es unitario para mi puede ser atómico y mi unitario abarca un poco más.
– Decir «pasos pequeños» implica una valoración respecto a lo que es pequeño o grande, puede que mi «pequeño» sea más grande que el de otros.
– Cuando decimos «hay que dejar que el diseño emerja» volvemos a meter valoraciones completamente arbitrarias. Tal vez a mi me emerge antes que a otro extraer una clase o plantear un refactor.
– Las palabras «complicado» o «complejo» también implican un juicio en el que a veces lo apropiado sería decir «no lo entiendo porque necesito aprender mas sobre X».
– Se nos llena la boca hablando de no sobrediseñar pero luego tenemos un pollo de dependencias inyectables que te cagas y terminamos usando un framework de inyección de dependencias porque nos duele cada vez que tenemos que tocar algo. Y todo porque construir cosas en el constructor de una clase está mal visto.
A veces me sorprende que, en una comunidad que fomenta tanto el cambio y la adaptación, tendamos a buscar definiciones tan categóricas respecto a algunos temas.
En fin, ya me he quedado a gusto… de momento ;)
Por ser claro y conciso: el tdd no existe.
O sea, no existe el diseño dirigido por tests. Como mucho se podria decir que puedes desarrollar un diseño previamente pensado (o asumido) apoyandote en tests. Los tests no dirigen nada de nada.
Para el resto de argumentos solo hay que repetir una de las ideas claves del post, que se ha diluido en los comentarios:
* «en realidad para eso necesitas tests, no hace falta usar TDD»: y donde pone «para eso» puedes poner el 90% de los argumentos de la tdd (el otro 10 es lo del diseño)
Entrando ya en los tests (que NO, repitamos todos, que NO la tdd) tanto para apoyar el desarrollo del diseño, como para asegurar la correccion del codigo y facilitar la refactorizacion exiten alternativas a que pueden ser mas utiles que los tests aislados segun el bendito contexto: tipos, la repl, tests sinteticos o basados en propiedades y un laaaaargo etc. Incluso, oye, pueden combinarse con los tests para que sean mas utiles todavia.
@jneira Sandro Mancuso tiene un post en el que habla del tema. Es un punto de vista interesante independientemente de que pueda coincidir o no con él: https://codurance.com/2015/05/12/does-tdd-lead-to-good-design/
Hay tantas cosas interesantes para comentar…
Pero una al menos: yo si aplico disenio usando TDD. Muchas veces no se como lo voy a ir implementando internamente, y voy agregando implementacion interna a medida que agrego tests. Y aplicando siempre buscar la simplicidad, no complicarla, buscando pasar el test de la forma mas simple.
Sobre diseño de software Kent Beck twitteo el 15 de Junio y no puedo estar más de acuerdo: esto y matizó después su intención.
Morajela: Si estas en medio de la selva y tienes GPS y mapa… ¡usalo! Si solo tienes un machete… TDD y a desbrozar la selva con paciencia.
¡Gran post! (como habitualmente ^^)
A mí personalmente usar TDD me es es útil. De todas formas, como comenta Aitor, no me obsesiona que el resto lo usen. Siempre y cuando hagan tests :P.
Volviéndome al tema de porque me es útil usarlo. Voy a poner algunos ejemplos:
– Me ayuda a pensar en el problema. Comentas que tú eres más de «pensar un rato antes». Pero creo que eso justamente es una de las partes fundamentales de TDD. El propio Kent Beck (y otros autores) en su libro, recomiendan que antes de escribir el primer test, pienses en el problema y extraigas un listado de ejemplos.
– Me da seguridad a la hora de construir un algoritmo. Ejemplos:
– Hace unos meses en mi trabajo (desarrollamos un software de reservas de viajes) tuve que hacer un parte del sistema que construía, dada una seria de opciones de habitaciones, las diferentes combinaciones para una habitación, dos habitaciones, etc. Sin prácticar TDD, me hubiera sido muy complicado.
– Hace unos meses fui a corregir una parte del proyecto encargado de añadir destinos al viaje. Fallaba una casuística de añadir un destino al final del viaje. La intenté corregir, pero rompí otra casuística. Al final lo borré todo, y empecé de cero haciendo TDD y añadiendo test a test todas las casuísticas que presentaba el problema.
* Cierto es que a esos dos ejemplos también hubiera sobrevivido simplemente haciendo tests. Pero a continuación también explico porque me va mejor hacer dichos tests practicando TDD :-)
– Me da más seguridad a la hora de construir los tests. Cuando tengo que refactorizar un código sin tests, previamente tengo que hacer dichos tests. Entonces cuando termino cada uno de los test, necesito cambiar algo del código para ver si el test falla. De hecho, he visto tests que realmente no funcionaban pero el que los hizo no se dio cuenta porque estaba en verde. Empezar el test en rojo, a mí me funciona mejor (de hecho, me parece más lógico).
– Sobre si mis diseños son mejores con TDD. Creo que hay que intentar escuchar a los tests. Si realmente se hace difícil testear algo, quizás es que tienes un problemas de diseño. Cuando haces los tests al final, quizás ya es muy tarde para cambiar algo (a veces es mejor empezar de cero). En cambio al hacer TDD, puedes detectar los problemas mucho antes (aunque también puede pasar que a veces salga más a cuenta empezar de cero).Pero bueno, tal como comenta Modesto, los post/charlas de Sandro Mancuso sobre el tema son muy buenos. Y creo que la fase de refactoring ayuda mucho a aplicar tus conocimientos de diseño.
Pero vaya, yo uso TDD cuando _creo_ que me aporta valor ^^. Y entiendo que si a otra persona no le aporta valor, pues que no lo use. Al final todos somos distintos y tenemos nuestro propio estilo.
Lo que no me gusta del debate de TDD sí, TDD no. Es que al igual que a mí no me obsesiona que otra personas lo usen o no. No entiendo porque algunos que no lo usan tienen la necesidad de imponer el criterio de «Como a mi no me aporta valor TDD, pues es que realmente no sirve para nada» (creo que no es tu caso).
Saludos!
Siempre desarrollo empezando por lo que quiero obtener («top-down» como has denominado). Esto permite que la implementación sea cómoda de consumir. El avance en el desarrollo lo realizo de forma iterativa: caso de uso, prueba y no necesariamente primero la prueba, de hecho es poco habitual que implemente la prueba en primer lugar. En mi caso he observado que crear primero pruebas suele generar código innecesario, con pruebas bastante alejadas de la implementación final.
Se puede decir que aplico TDD: Test During Development.
Mi opinión sobre el tema
Why Useful TDD is a Unicorn
https://dzone.com/articles/why-useful-tdd-is-a-unicorn?utm_content=buffer6d1b9&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer