José Cabrera 6 minutos

Qué es TDD y cómo funciona

Cuando desarrollamos una aplicación queremos que funcione y, para ello, necesitamos probar el código. Para probarlo pueden existir diferentes opciones:

  • Probar manualmente la aplicación una vez acabado el desarrollo, ya sea porque es un rollo estar continuamente probándolo o porque delegamos ese testeo en un equipo de QA.
  • Probar manualmente sólo lo que estamos desarrollando olvidándonos de hacer pruebas del resto de la aplicación.
  • Introducir tests automatizados que validan el funcionamiento de la aplicación a medida que introducimos código o realizamos cambios.

Los tests automáticos nos ayudan a crear código de calidad y hace que el software sea sencillo de mantener. Podemos crearlos antes de escribir el código de producción o después.

Si escribimos los tests después podríamos acabar en que:

  • Nos da pereza crearlos
  • El código que hemos creado cuesta de testear
  • Probamos lo mínimo olvidándonos de todas las casuísticas.

Por eso, en mi opinión, la mejor forma de asegurarnos que la mayor parte de nuestro código se prueba es hacer los test primero.

Entonces, ¿qué implica hacer los tests primero? No debemos hacer código y luego crear los tests, sino que introducimos el tests y luego creamos la implementación que haga que ese tests pase.

¿Qué es TDD?

TDD son las siglas de Test Driven Development o desarrollo dirigido por pruebas. Es una metodología de desarrollo.

Esta metodología no se basa en realizar una enorme batería de tests y después escribir el código, sino que es ir realizando pequeños ciclos de testing e ir escribiendo a su vez el código necesario para poner el test en verde.

Una vez lo tenemos en verde, quitar duplicación y refactorizar. Este ciclo es conocido como RED · GREEN · REFACTOR

Cuando hacemos TDD recomendamos respetar las siguientes normas:

  • Es necesario escribir un test que falle antes de añadir lógica de negocio.
  • No escribir más código de producción de lo necesario para hacer pasar una prueba unitaria.
  • No refactorizar si tenemos algún test en rojo.

Con esta práctica se consigue entre otras cosas: un código más robusto, más seguro, más mantenible y una mayor rapidez en el desarrollo.

Los tres pasos de TDD


Tdd cycle

Para entenderlo mejor, voy a hacer el Paso 1 de la kata String Calculator cuyo requisito es:

- Crear una calculadora de cadenas simple con una firma de método:
- El método puede recibir hasta dos números, separados por comas, y devolverá su suma (p. ej "" o "1" o "1,2" como entradas).
- Para una cadena vacía, devolverá 0.

1- Hacer un test automático de prueba, ejecutarlo y ver que falla. Debe fallar por el motivo correcto.

El primer tests sería algo así:

Este tests estaría fallando y ¿lo hace por el motivo correcto?

Add function is not defined as a test error message

Cuando creamos nuestro primer test no se trata de introducir tests que fallen por motivos incorrectos, se trata de que falle porque la funcionalidad esperada no es la correcta. Por suerte los tests nos ayudan en nuestro desarrollo y aquí nos está diciendo que la función add no está definida.

Para que el test de ejemplo funcione (falle por lo esperado) deberíamos definir la función add

Failing tests because return undefined instead of zero

Ahora sí estaría fallando por el motivo que esperamos (en JS no es un lenguaje tipado, por ello no se queja por el hecho de definir una función sin parámetros y al llamarla pasarle argumentos)

2- Introducir el código más simple posible para que el test que acabamos de escribir pase.

Te lanzo un reto, sin mirar la solución: ¿Cuál crees que es el código mínimo a introducir para hacer pasar el test?

En este caso la implementación mínima a introducir sería hacer que la función add nos devuelva un 0. De esa manera tendríamos los tests pasando.

Passing tests after first iteration

3- Refactorizar

Una vez puesto los tests en verde, podemos realizar cambios (refactor) con seguridad ya que tenemos unos tests que nos dan feedback constantemente.

En ésta fase revisamos el código en búsqueda de posibles «smells». En esta etapa podemos tomarnos nuestro tiempo para elegir las mejores decisiones posibles.

Cuando entramos en la fase de refactorización debemos tener en cuenta de que no debemos añadir nuevas funcionalidades.

Si volvemos al ejemplo de la calculadora, como de momento es un ejemplo muy sencillo, los refactors que podríamos hacer:

  • Llevar la función add a un fichero
  • Cambiar el nombre del tests

Estos tres pasos deben repetirse una y otra vez hasta que la aplicación esté terminada. Entonces si tuviéramos que terminar la aplicación de la calculadora. ¿Cómo continuaremos?

Repitiendo ciclo

1- Hacer un test automático de prueba, ejecutarlo y ver que falla:

Introducimos un nuevo tests

Failing tests after second iteration

2- Hacer el código mínimo imprescindible para que el test que acabamos de escribir pase.

Nos vamos al código de producción y de nuevo añadimos el código más simple posible para poner los tests en verde de nuevo.

3- Refactorizar

Volvemos a buscar patrones que nos permitan limpiar el código. Si no encontramos ningún patrón, podemos avanzar volviendo al primer paso del ciclo.

A partir de aquí, como ya tenemos claro cómo aplicar el ciclo, iré un poco más al grano.

Ciclo 1 y 2: Añado un nuevo tests, en éste caso para el caso en el que recibe un “2” y a su vez modifico el código de producción para hacerlo pasar.

3- Refactor

Aquí ya puedo ver un patrón claro:

Cuando el usuario me pasa un número en formato string, hago una conversión del string a numérico. Por tanto podría refactorizar el código para hacerlo más sencillo.

Si volviesemos a los requisitos de momento hemos añadido la funcionalidad de 2 sobre 3

  • Para el caso de string vacío “” debe devolver 0 -> Hecho
  • Para el caso de un único número (sin coma) devolver ese mismo número en formato numérico -> Hecho
  • Para el caso números separados por coma, debe devolver la suma de esos números -> NO hecho.

Vamos a atacar el último requisito pendiente.

¿Cómo lo haríamos?

  • Añadiendo un tests
  • Añadiendo el código más sencillo posible para que ese test pase.
  • Refactorizar si fuese necesario.

Podríamos implementar la solución a ese problema de la siguiente manera:

Podemos entrar en refactorizar el código para dejarlo un poco más limpio utilizando programación funcional.

Una vez implementado, cumpliríamos con los requisitos del primero paso de la Kata. Te animo a continuar si así lo deseas :).

Te podría entrar la duda: lo que hemos hecho es sencillo, porque al final es una función pura y es fácil testearla. ¿Qué pasa cuando trabajo por ejemplo con React? ¿Es factible aplicar ésta metodología en un proyecto con React?. La respuesta es sí, la respuesta larga…. lo veremos en otro post.

Además te invito a resolver nuestro challenge de TDD en el que aterrizamos los conceptos vistos en éste post. ¡Animate!

La parte 2 de Test-Driven Development la puedes encontrar en: TDD React.