La esencia de TDD es sencilla pero ponerla en práctica correctamente es cuestión de entrenamiento, como tantas otras cosas. El algoritmo TDD sólo tiene tres pasos:
- Escribir la especificación del requisito (el ejemplo, el test).
- Implementar el código según dicho ejemplo.
- Refactorizar para eliminar duplicidad y hacer mejoras.
Veámosla en detalle.
2.1.1. Escribir la especificación primero
Una vez que tenemos claro cuál es el requisito, lo expresamos en forma de código. Si estamos a nivel de aceptación o de historia, lo haremos con un framework tipo Fit, Fitnesse, Concordion o Cucumber. Esto es, ATDD. Si no, lo haremos con algún framework xUnit.
¿Cómo escribimos un test para un código que todavía no existe? Respondamos con otra pregunta ¿Acaso no es posible escribir una especificación antes de implementarla? Por citar un ejemplo conocido, lasJSR (Java Specification Request) se escriben para que luego terceras partes las implementen... ¡ah, entonces es posible!. El framework Monose ha implementado basándose en las especificaciones del ECMA-334 y ECMA-335 y funciona. Por eso, un test no es inicialmente un test sino un ejemplo o especificación.
La palabra especificación podría tener la connotación de que es inamovible, algo preestablecido y fijo, pero no es así. Un test se puede modificar. Para poder escribirlo, tenemos que pensar primero en cómo queremos que sea la API del SUT, es decir, tenemos que trazar antes de implementar. Pero sólo una parte pequeña, un comportamiento del SUT bien definido y sólo uno.
Tenemos que hacer el esfuerzo de imaginar cómo seria el código del SUT si ya estuviera implementado y cómo comprobaríamos que, efectivamente, hace lo que le pedimos que haga. La diferencia con los que dictan una JSR es que no diseñamos todas las especificaciones antes de implementar cada una, sino que vamos una a una siguiendo los tres pasos del algoritmo TDD.
El hecho de tener que usar una funcionalidad antes de haberla escrito le da un giro de 180 grados al código resultante. No vamos a empezar por fastidiarnos a nosotros mismos sino que nos cuidaremos de diseñar lo que nos sea más cómodo, más claro, siempre que cumpla con el requisito objetivo. En los próximos capítulos veremos cómo mediante ejemplos.
2.1.2. Implementar el código que hace funcionar el ejemplo
Teniendo el ejemplo escrito, codificamos lo mínimo necesario para que se cumpla, para que el test pase. Típicamente, el mínimo código es el de menor número de caracteres porque mínimo quiere decir el que menos tiempo nos llevó escribirlo. No importa que el código parezca feo o chapucero, eso lo vamos a enmendar en el siguiente paso y en las siguientes iteraciones.
En este paso, la máxima es no implementar nada más que lo estrictamente obligatorio para cumplir la especificación actual. Y no se trata de hacerlo sin pensar, sino concentrados para ser eficientes. Parece fácil pero, al principio, no lo es; veremos que siempre escribimos más código del que hace falta.
Si estamos bien concentrados, nos vendrán a la mente dudas sobre el comportamiento del SUT ante distintas entradas, es decir, los distintos flujos condicionales que pueden entrar en juego; el resto de especificaciones de este bloque de funcionalidad. Estaremos tentados de escribir el código que los gestiona sobre la marcha y, en ese momento, sólo la atención nos ayudará a contener el impulso y a anotar las preguntas que nos han surgido en un lugar al margen para convertirlas en especificaciones que retomaremos después, en iteraciones consecutivas.
2.1.3. Refactorizar
Refactorizar no significa reescribir el código; reescribir es más general que refactorizar. Segun Martín Fowler, refactorizar es "modificar el diseño sin alterar su comportamiento". A ser posible, sin alterar su API pública.
En este tercer paso del algoritmo TDD, rastreamos el código (también el del test) en busca de líneas duplicadas y las eliminamos refactorizando. Además, revisamos que el código cumpla con ciertos principios de diseño (me inclino por S.O.L.I.D) y refactorizamos para que así sea.
Siempre que llego al paso de refactorizar, y elimino la duplicidad, me planteo si el método en cuestión y su clase cumplen el Principio de una Única Responsabilidad y demás principios. El propio Fowler escribió uno de los libros más grandes de la literatura técnica moderna en el que se describen las refactorizaciones más comunes. Cada una de ellas es como una receta de cocina.
Dadas unas precondiciones, se aplican unos determinados cambios que mejoran el diseño del software mientras que su comportamiento sigue siendo el mismo. Mejora es una palabra ciertamente subjetiva, por lo que empleamos la métrica del código duplicado como parámetro de calidad. Si no existe código duplicado, entonces hemos conseguido uno de más calidad que el que presentaba duplicidad.
Mas allá de la duplicidad, durante la refactorización podemos permitirnos darle una vuelta de tuerca al código para hacerlo más claro y fácil de mantener. Eso ya depende del conocimiento y la experiencia de cada uno. Los IDE como Eclipse, Netbeans o VisualStudio, son capaces de llevar a cabo las refactorizaciones más comunes. Basta con señalar un bloque de código y elegir la refactorización Extraer-Método, Extraer-Clase, Pull-up, Pull-down o cualquiera de las muchas disponibles. El IDE modifica el código por nosotros, asegurándonos que no se cometen errores en la transición.
Al margen de estas refactorizaciones, existen otras más complejas que tienen que ver con la maestría del desarrollador y que a veces recuerdan al mago sacando un conejo de la chistera. Algunas de ellas tienen nombre y están catalogadas a modo de patrón y otras son anónimas pero igualmente eliminan la duplicidad. Cualquier cambio en los adentros del código, que mantenga su API pública, es una refactorización.
La clave de una buena refactorización es hacerlo en pasitos muy pequeños. Se hace un cambio, se ejecutan todos los tests y, si todo sigue funcionando, se hace otro pequeño cambio. Cuando refactorizamos, pensamos en global, contemplamos la perspectiva general, pero actuamos en local. Es el momento de detectar malos olores y eliminarlos.
El verbo refactorizar no existe como tal en la Real Academia Española pero, tras discutirlo en la red, nos resulta la mejor traducción del término refactoring. La tarea de buscar y eliminar código duplicado después de haber completado los dos pasos anteriores, es la que más tiende a olvidarse. Es común entrar en la dinámica de escribir el test, luego el SUT, y así sucesivamente olvidando la refactorización. Si de las tres etapas que tiene el algoritmo TDD dejamos atrás una, lógicamente no estamos practicando TDD sino otra cosa.
Otra forma de enumerar las tres fases del ciclo es:
- Rojo
- Verde
- Refactorizar
Es una descripción metafórica ya que los frameworks de tests suelen colorear en rojo aquellas especificaciones que no se cumplen y en verde las que lo hacen. Así, cuando escribimos el test, el primer color es rojo porque todavía no existe código que implemente el requisito. Una vez implementado, se pasa a verde. Cuando hemos dado los tres pasos de la especificación que nos ocupa, tomamos la siguiente y volvemos a repetirlos. Parece demasiado simple, la reacción de los asistentes a mis cursos es mayoritariamente incrédula y es que el efecto TDD sólo se percibe cuando se practica.
Me gusta decir que tiene una similitud con un buen vino; la primera vez que se prueba el vino en la vida, no gusta a nadie, pero a fuerza de repetir se convierte en un placer para los sentidos. Connotaciones alcohólicas a un lado, espero que se capte el mensaje.
¿Y TDD sirve para proyectos grandes? Un proyecto grande no es sino la agrupación de pequeños subproyectos y es ahora cuando toca aplicar aquello de divide y vencerás. El tamaño del proyecto no guarda relación con la aplicabilidad de TDD. La clave está en saber dividir, en saber priorizar. De ahí la ayuda de Scrum para gestionar adecuadamente el backlog del producto. Por eso tanta gente combina XP y Scrum. Todavía no he encontrado ningún proyecto en el que se desaconseje aplicar TDD.
En la segunda parte del libro se expondrá el algoritmo TDD mediante ejemplos prácticos, donde iremos de menos a más, iterando progresivamente. No se preocupe si no lo ve del todo claro ahora.