Después de un tiempo pensando qué tipo de artículo escribir sobre TDD y, dado que hay muchos y muy buenos sobre teoría (escritos por referentes del mundo del desarrollo), he optado por desarrollar un mini proyecto mientras explico los puntos clave del desarrollo de la aplicación aplicando TDD.

La aplicación será un gestor de notas con usuarios donde podremos ver qué y cómo testeamos cada capa de nuestra aplicación aplicando TDD.

Me gustaría resaltar que el propósito del artículo es ver como TDD nos va a ayudar a desarrollar código limpio que funciona. TDD nos va a aportar un espacio continuado de diseño.

 

Teoría

Primero vamos a recordar algo muy básico de teoría sobre unit testing y TDD. Lo primero y mas importante es que TDD y unit testing son cosas diferentes. TDD es un proceso de desarrollo basado en obtener un feedback lo mas rápido posible a la hora de diseñar nuestra aplicación mientras que unit testing es una herramienta donde probar que un “unit” funciona como esperas.

La definición de unit testing es algo ambigüa, la gente diverge en la definición, sobre todo por la parte de “unit“. La gente hace un mapeo interno de unit a una función o clase cuando no debería ser así, unit se refiere a una funcionalidad/caso de uso. Esta funcionalidad puede implicar comunicación entre varios métodos/entidades.

TDD, sin embargo, no es tan ambigüo, se basa en dos reglas muy simples que siguiendolas nos llevará por el buen camino:

  • Escribir código solo cuando tengamos un unit test que falle
  • Refactorizar/Eliminar duplicación

 

Aplicando TDD

El mecanismo de TDD es fácil, escribir un test que pruebe la funcionalidad que queremos implementar (un caso de uso), acto seguido escribir el mínimo código posible para poner el test en verde. Una vez lo tenemos en verde, quitar duplicación y refactorizar. Fácil.

TDD es un mecánismo de feedback rápido, el ciclo en el cual uno desarrolla RED · GREEN · REFACTOR y las tres leyes en las que se basa:

  • You can’t write any production code until you have first written a failing unit test.
  • You can’t write more of a unit test than is sufficient to fail, and not compiling is failing.
  • You can’t write more production code than is sufficient to pass the currently failing unit test.

Todos sabemos que tener un bug en nuestro código y no encontrarlo es doloroso pero, más doloroso es cuando tienes un bug en un test. El test te da en verde, confías en el test pero el código lo ves bien, cambias código de producción que funciona generando un bug real y sigues sin entenderlo… la perdida de tiempo y la desesperación cuando pasa esto es brutal. Dentro del ciclo de TDD, cuando vemos el test en rojo y lo pasamos a verde controlamos que nuestro test realmente hace lo que debe hacer evitando este tipo de problemas.

 

Green Bar Patterns

Existen ciertos patrones para ayudarnos a poner nuestro test en verde de la manera mas rápida posible

1. Fake It

Devolver una constante y poco a poco ir cambiandolas por variables. Con esta técnica iremos dando muchos pasos muy pequeños y conseguimos poner el test en verde lo mas rápido posible

Ejemplo:

 
  expect(suma(1,2)).toEqual(3);

  function suma(a,b) {return 3}

 

Una vez lo tenemos en verde pasamos a la fase de Refactor, en este caso vamos a quitar duplicación. La duplicación en este caso no es de código sino de datos, tenemos datos repetidos que a simple vista no se ve, se verá mejor con este pequeño cambio:

 
  expect(suma(1,2)).toEqual(3);

  function suma(a,b) {return 1+2}

 

Realmente hemos puesto devuelto un 3 pero lo que queríamos hacer es 1+2, que son los mismos números que hemos pasado como parametros a nuestra función, ahora vemos la duplicación mas clara, vamos a quitarla:

 
  expect(suma(1,2)).toEqual(3);

  function suma(a,b) {return a+b}

 

Done! Mas adelante explicaremos como sacar la misma implementación con una técnica algo mas conservadora.

 

2. Obvious Implementation

En sí la técnica se basa en picar la implementación que nosotros creemos saber y confirmarla lo mas rápido posible. Esto suele llevar a picar menos test, lo cual de primeras puedes pensar que es positivo ya que vas mas rápido pero no lo es tanto cuando piensas que en la fase de Refactor, si no tienes testeado todas las especificaciones del SUT, puedes romper algo sin enterarte.

Con la implementación obvia lo que se busca (o mejor dicho, lo que se encuentra) es acelerar un poco el ciclo saltándonos uno de los pasos muy importante y es escuchar a nuestro test. Cuando nosotros picamos un test y esta en rojo, nos forzamos a preguntarnos como debemos implementarlo, dudamos de nuestra solución pero tenemos un mecanismo que nos va diciendo si lo vamos haciendo bien o mal. Con la implementación obvia, este paso, nos lo saltamos, vamos directos a implementar lo que tenemos en la cabeza sin dudar, bien podríamos picar el test después de implementar el algoritmo y tendríamos el mismo resultado.

Es aconsejable aplicar esta técnica cuando la implementación no solo es obvia sino que es trivial, donde existen menos especificaciones y corremos menos riesgo de fallo, aún así, siempre hay que andar con mucho cuidado.

Resumiendo, como implementarías operaciones simples? Simplemente implementalas.

Ejemplo:

 
  expect(suma(1,2)).toEqual(3);

  function suma(a,b) {return a+b}

 

3. Triangulate

Como dice Kent Beck en su libro TDD By Example, triangular es la técnica mas conservadora para conseguir la implementación que buscamos y no le falta razón. De primeras es lo mismo que Fake It, escribimos un test y lo ponemos en verde devolviendo una constante. Lo siguiente sería hacer el refactor pero no lo vamos hacer ahora, teniendo nuestro test en verde, lo que vamos a hacer es escribir otro test que lo ponga rojo:

Ejemplo:


  expect(suma(1,2)).toEqual(3);
  expect(suma(3,4)).toEqual(7);

  function suma(a,b) {return 3}

 

Ahora tendríamos dos caminos:

  • Desarrollar la implementación para tener los dos test en verde
  • Seguir devolviendo constantes con una implementación mas sencilla que la real
 
   function suma(a,b) {if(a===1) return 3 else return 7}

 

Una vez tengamos nuestra implementación, podemos llegar a pensar en eliminar los tests de mas que hemos usado para llegar a nuestra implementación, es posible que hayamos creado tests redundantes pero eso ya es algo que depende de cada caso y de cada persona.

 

Que técnica usar?

Bueno, esto ya es algo personal y subjetivo, algo que con la experiencia evoluciona. Beck comenta en el libro que lo que tenemos que conseguir es un ritmo de desarrollo rápido, red/green/refactor contínuo, si sabes lo que tienes que picar, utiliza Obvious implementation. Si no lo sabes, usa Fake It y si te quedas atascado en el diseño, termina triangulando.

Tras varios debates internos en Apiumhub (por email, openspaces y charlas informales a la hora de la comida), el como ponerlo en verde es algo personal (aunque fake it/triangulate suelen ser las técnicas mas utilizadas) pero si llegamos a una conclusión clara, y es no dejar ningúna especificación sin un test, algo que con implementación obvia es muy fácil de saltarse.

 

Aplicando TDD: Feedback

TDD es un proceso donde conseguir feedback sobre nuestro diseño de una manera rápida (lo voy a repetir las veces que haga falta), el feedback nos los van a dar los tests automatizados, entonces, cuantos tests necesitamos para estar seguros de que nuestro código funciona? Uno podría pensar que la confianza a la hora de desarrollar es el factor a a tener en cuenta para saber cuando parar de hacer tests, y, aunque tiene su importancia, es algo muy subjetivo. Como podemos convertir esa subjetividad en algo objetivo? como ya hemos comentado, un test debe cubrir una especificación, si tenemos test de todas las especificaciones que tenemos en mente, convertimos la subjetividad de la confianza personal en confianza a nivel de negocio.

Hay que enfatizar que TDD nace como un proceso dentro de una metodología ágil (Extreme Programming), los requerimientos que nos piden cambiarán, aparecerán nuevos, etc, los requerimientos se descubren a lo largo de todo el ciclo de desarrollo, incluso una vez subido a producción se descubrirán nuevos requerimientos.

Cuando nos encontramos un caso (que son muy pocos) en el que [creemos conocer] conocemos a la perfección la implementación, es trivial y creemos tener confianza ciega con ella, nos sale a cuenta dedicarle algo de tiempo escribir los tests para cubrir las especificaciones. No existe la excusa de que te lleva tiempo extra porque una de dos: o tienes experiencia con TDD o no la tienes. Si tienes experiencia, directamente te va a salir escribir tests y si no la tienes, prácticar es la única manera de llegar a entender bien TDD. Puedes leer mucho y pensar muy bien sobre TDD pero la experiencia es lo único que te hará diferente a los demás.

aplicando TDD

 

(Image source: XP Explained)

 

Y no te olvides de suscribirte a nuestro boletín mensual para recibir más ejemplos de desarrollo aplicando TDD . 

 

Si te gustó este artículo sobre ejemplo de desarrollo aplicando TDD, te puede gustar:

 

Notas sobre DDD Europe

Arquitectura de microservicios vs arquitectura monolítica 

Scala generics I: clases genéricas y type bounds 

Scala generics II: covarianza y contravarianza 

Principio de responsabilidad única 

Por qué Kotlin?

Patrón MVP en iOS

F-bound en Scala

Sobre Dioses y procrastinación

Arquitectura de microservicios

Fibers en Nodejs

Simular respuestas del servidor con Nodejs

Mapa de los “main players”: ecosistema startup y tech en Barcelona

Ecosistema de salud digital en Barcelona