TDD, Asimov y la ley cero
Introducción
Es este post veremos una rápida introducción a Test-Driven-Development (TDD). Queremos explorar algunas cosas que hemos aprendido durante este apasionante viaje y que esperamos que os guste. ¿Quién sabe? Puede que te unas a este culto oscuro.
¿Qué es Test Driven Development?
TDD o Test Driven Development es una técnica de programación que consiste en escribir primero una prueba y después el código de ‘producción’. Esto se consigue repitiendo una y otra vez tres pasos:
- Escribir una prueba que falle para el código que queremos desarrollar.
- Escribir el código mínimo que hace que el test pase (ponerlo en verde).
- Refactorizar el código manteniendo la prueba en verde.
Después de mucho tiempo practicándolo, Uncle Bob describe tres reglas simples para escribir código (por favor, lee el artículo enlazado; es muy interesante, lo prometo).
Las tres reglas son:
- No puedes escribir código de producción a no ser que sea para que una prueba pase a verde.
- No puedes escribir más de lo necesario para que una prueba unitaria falle; y un fallo de compilación cuenta como fallo.
- No puedes escribir más código que el necesario para que una prueba que está fallando pase a verde.
Hay un montón de cosas interesantes tanto en los tres pasos de TDD como en las reglas de Uncle Bob. Veamos algunas de ellas.
Ciclo de TDD
Sí, ya, primero la prueba, pero ¿qué pruebo?
Es una pregunta típica cuando quieres empezar esta nueva vida. La respuesta a esta cuestión es muy simple: prueba el código que quieres añadir. No te rías, por favor; inténtalo.
El ciclo de TDD puede parecer fácil, pero la primera prueba de un proyecto siempre es la más difícil. No importa si estamos en un proyecto nuevo, o si hemos caído en un proyecto que ya lleva tiempo en marcha. Es la más difícil porque necesitamos crear todo lo necesario para escribirla; esto es, crear el proyecto de pruebas donde ‘vivirá’, directorios, infraestructura, etc. Pero una vez que lo tengas, añadir otra prueba es realmente fácil.
Recuerda la frase de Uncle Bob: “Un fallo de compilación cuenta como fallo”. Que no cunda el pánico. Estás en el ciclo. Termina el scaffolding, y ve al siguiente paso, que debería ser tu primera prueba.
Escribe la prueba que instancia una variable de una clase que aún no existe, esto es, un test que falla. Ahora crea la clase. Dale un nombre; el test está en verde, tu sistema ‘funciona’. Ahora invoca el método en donde quieres añadir la lógica; aún no existe, ¡perfecto! El sistema está fallando, pero, hace unos segundos, estaba funcionando. Usa las herramientas que te da tu IDE para crear ese método que no hace nada… Genial, está en verde, el sistema vuelve a funcionar.
Una vez más, no tengas miedo: has tomado decisiones y estás teniendo feedback instantáneo. ¿El nombre de la clase encaja? ¿Y el nombre del método? ¿Los parámetros? ¿Los tipos que devuelven? ¿Sí? ¡Genial! ¿No? ¡Mejor! Tienes feedback de que algo no va bien, puede que tengas que repensar algún nombre…
¡Ala! Espera un momento…
No has generado una versión del sistema, ni desplegado a producción, ni dejado que un usuario lo ejecute, y has visto que hay algo raro. Tienes feedback instantáneo en tiempo de desarrollo de que algo no está bien. ¿Aún piensas que es una pérdida de tiempo? Con lo que has aprendido puedes volver atrás e intentar otra cosa, pero esta vez tienes más información. Vuelve atrás, empieza de nuevo, con tan solo dos o tres líneas de código has aprendido cosas y ahora es más probable que tengas éxito en el próximo intento.
Haz que la prueba pase
En este punto, ya tienes una prueba que está fallando. Normalmente, el código de la prueba tendrá más o menos esta forma:
- Una instancia de la clase que quieres probar (Arrange).
- Llamar a un método (Act).
- Comprobar si la lógica está haciendo lo que debe (Assert).
Ahora es el momento de escribir el código que hace que el test pase. Solo el código mínimo que hace que la prueba pase. Un ejemplo puede ser un simple “devuelve true”. No tengas miedo, no hemos terminado el ciclo, aún no hemos hecho el paso de refactorización. Y como TDD es un proceso iterativo, la próxima prueba que escribas hará que modifiques el ‘código de producción’, así que no tengas miedo de dar un ‘salto de fe’: como ya has leído en el artículo de Uncle Bob, el código estaba funcionando sin errores hace unos minutos. A esto se le conoce como “dar baby steps”; hablaremos de ellos más adelante.
Intenta Assert-Act-Arrange en lugar de Arrange-Act-Assert
Hemos visto el patrón típico para escribir buenas pruebas, Arrange-Act-Assert. ¿Y si lo hacemos al revés?
Assert
Piensa cómo vas a comprobar el código que quieres probar. ¿Comprobar un valor en una propiedad? ¿El valor que devuelve un método? Piénsalo y escribe primero el assert. Estás tomando decisiones y obteniendo feedback de si merece la pena hacerlo así.
Act
Ejecuta la acción, invoca el método para que se ejecute el código y haga lo suyo. Ya tienes cómo comprobar si lo hace bien. Aquí estás decidiendo cómo vas a invocarlo, con qué parámetros, con qué nombre, etc.
Arrange
Instancia tu objeto con las dependencias necesarias.
En cada ‘sección’ de la prueba estás desarrollando, diseñando y experimentando con el código que tienes en tu cabeza. Aún no está escrito, pero ya tienes varios mecanismos para comprobar si lo que tienes en la cabeza encaja en el sistema. Y si algo parece raro, o ves que se te ha pasado algo, ¡hurra! Da un paso atrás y prueba otra vez. Eres como el Doctor Strange con el Ojo de Agamotto.
TDD no va sobre velocidad sino sobre frecuencia: en el feedback, ejecutando el código, comprobando si algo se ha roto…
Refactoriza el código que has añadido
Bueno, ya hemos conseguido que la prueba pase de rojo (fallo) a verde (pasa). Ahora es el momento de ver si el código que hemos escrito es fácil de leer, eliminar duplicidades, buscar bad smells… Este paso suele ser para mí el más difícil, porque tenemos que hacer esos cambios teniendo en cuenta dos cosas:
- No romper la prueba que hemos escrito, ni ninguna otra.
- No implementar más lógica de la necesaria.
La funcionalidad que queremos desarrollar aún no está terminada, así que volvemos de nuevo al primer paso y creamos otra prueba. Así hasta que terminemos la funcionalidad.
Fácil, ¿verdad? Spoiler: no, no lo es. Es más fácil decirlo que hacerlo.
Lo que he aprendido
Estos pasos parecen fáciles, pero hay un montón de conocimiento oculto detrás de todo esto. Déjame que te enseñe lo que he aprendido de esas leyes, del ciclo y de practicarlo una y otra vez.
No hay balas de plata, pero sí balas trazadoras
TDD no es una bala de plata, me recuerda más a una bala trazadora, como las que se describen en ‘The Pragmatic Programmer’ [2].
Es una forma de dar esos famosos baby steps y obtener feedback de si vamos en la dirección correcta. Cuando descubrimos que ese no es el camino, damos un paso atrás, y damos uno nuevo en otra dirección. Como un mandaloriano diría: “Este es el camino”.
No, en serio, ¿qué debo probar?
Si no estás seguro de hacer una prueba para el código que quieres añadir… Ese no es el camino.
Un buen punto de partida puede ser cuando corregimos un bug. Una vez que hemos encontrado la razón por la que ocurre, normalmente tras un tiempo de debugging, no lo arregles inmediatamente. Primero intenta crear una prueba que lo reproduzca y céntrate en ver cómo hay que instanciar la clase, qué valores tienen las propiedades, qué método se invoca… Suele ocurrir que es complicado instanciar la clase, hay algunas dependencias que nos fastidian. “Bienvenido a mi mundo, Neo”.
Toma nota de la prueba que quieres crear. Ahora, intenta desacoplar la dependencia y haz tu magia, extrae la dependencia a un método, luego a una clase, etc. Sé creativo. Mientras estás haciendo eso, añade otra nota para probar el código que acabas de refactorizar. La clave está en no perder el foco de lo que estás haciendo ahora mismo.
Karate Kid y premios Nobel
¿Recuerdas la película ‘Karate Kid’? ¿Te acuerdas lo que decía el señor Miyagi a DanielSan? “Dar cera, pulir cera”. Parecía fácil, pero para DanielSan no tenía sentido ese ejercicio para aprender karate. Es muy parecido a hacer TDD las primeras veces: si persistimos en la práctica, tarde o temprano veremos la verdad que esconde.
¿Has oído la cita de Kent Beck: “Primero hazlo funcionar, luego hazlo bien” [3]?
En el desarrollo de software sabemos que la primera versión del código que escribimos para resolver un problema no suele ser la mejor solución. Solemos hacer iteraciones, y en cada una de ellas modificamos el código hasta que conseguimos el comportamiento que queremos.
Daniel Kahneman [4], un psicólogo y economista premio Nobel de Ciencias Económicas, habla sobre cómo funciona el cerebro humano en su libro ‘Pensar rápido, pensar despacio’. El cerebro humano tiene dos modos, el lento y el rápido. El rápido está diseñado para dar respuestas rápidas a preguntas y situaciones; el lento es el modo analítico.
El modo rápido es el resultado de la evolución para reducir el consumo de recursos a la hora de tomar decisiones que permiten al individuo sobrevivir ante determinadas situaciones. Cuando, por ejemplo, escuchamos un ruido en un arbusto que tenemos a nuestra espalda, tenemos la necesidad de escondernos de algún peligro. Hoy en día ya sabemos que es muy raro que haya un león que nos esté acechando para comernos. Este es el modo lento del cerebro en acción.
Hay una relación entre las palabras de Kent Beck y Daniel Kahneman. Mientras estamos programando, primero intentamos que funcione y después lo hacemos bien refactorizando, separando responsabilidades, creando abstracciones, etc.
TDD nos ayuda deteniendo ese modo rápido de nuestro cerebro y despertando al modo lento para que empiece a trabajar desde el principio. Escribiendo primero la prueba, el modo lento toma el control. Empezamos a pensar en nombres de clases, métodos, parámetros, en cómo vamos a comprobar que el código hace lo que queremos, etc. El resultado es que el código no solo hace lo que queremos, sino que además es código legible por un ser humano.
Proyecto nuevo, legacy y pérdida de tiempo
Cuando empezamos un proyecto nuevo somos capaces de entregar funcionalidad muy rápido. El modo rápido del cerebro está activo, creemos que estamos estableciendo unas buenas bases porque no vamos a cometer los mismos errores del proyecto anterior. Es un proyecto nuevo, no tenemos tiempo para pruebas, tenemos que cerrar funcionalidades ASAP, llevamos una buena velocidad… De repente, un día, la velocidad decae, no somos capaces de cerrar tantas funcionalidades como solíamos hacer, y el proyecto nuevo se transforma en un proyecto legacy. En el equipo se empieza a decir que deberíamos parar y empezar de nuevo. Ala, un déjà vu. ¿Qué hemos hecho mal esta vez?
“Primero haz que funcione, luego hazlo bien” (dar cera, pulir cera).
El modo rápido del cerebro es bueno en hacer que las cosas funcionen rápidamente, pero ¿le hemos dejado al modo lento que lo ‘haga bien’?
Aquí es donde tienen sentido las leyes de TDD. Permiten al modo lento trabajar. En el libro ‘TDD by example’, Kent Beck recomienda crear una lista de las pruebas que vamos a hacer antes de empezar. Yo no entendía los beneficios de eso hasta pasado un tiempo. Ese paso tiene dos objetivos:
- Despertar al modo lento del cerebro y empezar a obtener feedback de los primeros pensamientos y decisiones sobre la solución que estamos diseñando.
- Foco. Empezamos una lista de tareas que nos mantienen centrados en una sola prueba a la vez. Déjame explicar este punto en la siguiente sección.
Foco. La ley cero
En un proyecto nuevo es fácil tener toda la ‘lógica’ en nuestra cabeza y desarrollar las funcionalidades rápidamente. La complejidad del sistema es manejable al principio. Pero con el tiempo esa complejidad aumenta exponencialmente.
Tener una lista de pruebas que quieres añadir en un ‘papel’ nos permite centrarnos solo en la prueba actual, ya que no tenemos que preocuparnos por el resto del sistema. Ya está probado, y hace un minuto, ¡todo estaba funcionando [1]! Si rompes algo, está en el código que acabas de añadir.
Lo que obtenemos haciendo pruebas de este modo es un proyecto que siempre tiene pinta de nuevo. Todo estaba funcionando hace un momento, así que el miedo de desplegar un viernes por la tarde ‘desaparece’. Ala, eso ha escalado muy rápido, Juanma. Bueno, vale, tienes la confianza necesaria como para desplegar un viernes por la tarde, pero NO LO HAGAS.
A esto le llamo la ley cero de TDD, como la de Asimov [5]. Antes de escribir el código del test, escribe el test en un papel.
¿Es más rápido?
No tengo dudas, pero tampoco pruebas. No he contado las veces que NO me he quedado hasta las 3 de la mañana corrigiendo un bug en producción.
¿Qué crees que es más rápido? Añadir funcionalidades a:
- Un proyecto nuevo con las funcionalidades previas probadas unitariamente como resultado de aplicar TDD.
- Un proyecto que no tiene pruebas para comprobar si has roto algo.
Es interesante reflexionar sobre ello. Sí, pero no llegamos, nos quedamos sin tiempo. El tiempo, un bien tan preciado. Esa es la razón por la que testeamos nuestro código. Es un error muy común decir: “No hacemos test porque no tenemos tiempo”. Cuando empezamos a hacer test nos damos cuenta muy pronto de que es exactamente al contrario: “Hacemos test porque NO tenemos tiempo”.
Puede que esta imagen nos ayude a comprenderlo:
Resumen
Espero que encuentres útil y divertido este post. En realidad, no importa si practicas TDD o no: lo importante es que tengas tu código probado. Y sí, no importa si esas pruebas son buenas o malas. Prefiero una prueba mala que se ejecuta en cada ciclo de integración (CI) que una prueba perfecta que no está ni escrita ni se ejecuta.
Ese es el primer paso. Después de un tiempo, la evolución natural es que termines practicando TDD; es solo cuestión de tiempo.