Introduccion a TDD con ejemplos practicos.

Victor Paredes
codeAndreani
Published in
7 min readApr 26, 2022

Aunque parezca mentira la mayor parte de las metodologías de trabajo en el desarrollo de software existen hace más de 30 años. Desde el nacimiento de scrum en los años 80, pasando por el manifiesto Agile y extremme programming en los 90 y a principios del 2000 ha pasado mucha gente que logró desarrollar técnicas y metodologías que siguen aun vigente.

Test Driven Development (TDD de ahora en más) es una técnica de desarrollo que es parte de Extreme Programming y nos ayuda a desarrollar software evitando la complejidad y permitiendo el diseño evolutivo.

Explicación resumida.

En palabras muy simples, TDD es una técnica de desarrollo que nos permite crear aplicaciones con un diseño evolutivo. ¿Qué quiere decir esto? que aplicando TDD vamos a poder comenzar a codear sin tener el diseño final de nuestra aplicación ya que el mismo surgirá a medida que vayamos escribiendo pruebas.

TDD no es una herramienta de testing ni de QA, aunque así lo parezca su principal objetivo es que el diseño de nuestro código sea pequeño, simple y fácil de entender

¿Cómo funciona?

La complejidad se encuentra en el uso y la práctica ya que es un cambio radical en la forma de trabajo. Los conceptos básicos son muy simples, son tres pasos que hay que respetar, las reglas son pocas y sencillas.

  • RED: Escribir una prueba unitaria que falle.
  • GREEN: Escribir el mínimo código productivo para que la prueba unitaria deje de fallar.
  • REFACTOR: Mejorar el código existente.

Escribiendo una prueba unitaria que falle.

El primer paso en TDD es pensar todos los casos de uso que va a tener nuestro componente y cual es el comportamiento que esperamos para cada uno. Por ejemplo, tomemos una agenda en la cual vamos a buscar un destinatario por email, se me ocurren los siguientes casos de uso:

  1. Al buscar con un email invalido debere avisar que el email es invalido.
  2. Al buscar un destinatario inexistente con un email válido deberé informar que el destinatario no existe.
  3. Al buscar un destinatario existente con un mail válido deberá devolver el destinatario que corresponde con la búsqueda.

Vamos a llamar a esta lista “Test List”. Tomemos el primer caso para analizarlo (luego hagan mentalmente lo mismo con los demás). Podemos dividir la frase en dos partes.

Un escenario perfectamente descripto: “Al buscar con un email invalido”

Un resultado esperado claro y preciso: “debere avisar que el email es invalido”

El escenario descripto debe ser lo mas preciso posible, no es lo mismo “al buscar un email” que “al buscar un email invalido” o “al buscar un email que no existe”. La precision de este enunciado ayudara al proximo desarrollador (que podemos ser nosotros) a entender el caso de uso.

Podemos confiar que El resultado esperado es el correcto si podemos pasarlo a pregunta y responderlo con un verdadero / falso. ¿Recibi un aviso de email invalido?.

¿De donde sale la inspiración para escribir estos casos de uso? De los requerimiento de nuestro cliente. TDD se lleva muy bien con Scrum y las User Story con lo cual podemos tomar una historia de usuario y revisar sus criterios de aceptación para comenzar a escribir nuestra lista. No debemos limitarnos, si encontramos más casos de uso que resulten útiles debemos escribirlos.

Nuestro cliente necesita, en este ejemplo, buscar por dirección de email a los destinatarios con lo cual me imagino una User Story asi:

Yo como usuario que debe enviar un mensaje necesito poder validar los emails de destinatarios para poder asegurarme que mis mensajes lleguen a destino.

Criterios de aceptación:

- Debo informar cuando la dirección de email sea inválida o vacía.

- Deberá devolver el primer destinatario que exista con ese email (la base de datos puede tener emails repetidos).

- Deberá informar si el email que se busca no coincide con ningún destinatario.

EJERCICIO 1: Verificar los casos de uso anteriores y detectar al menos un caso de uso mas. (SPOILER ALERT: yo conté al menos dos)

Ahora que tenemos una “Test list” podemos comenzar a escribir el primer Test Unitario. Tomamos de la lista el caso más fácil de hacer y escribimos un test unitario. Debemos dividir el test en tres partes.

Preparación: En este punto creamos todo lo necesario para recrear el entorno que el código productivo va a tener para cumplir con el escenario propuesto

Ejecución: Ya con el entorno preparado, hacemos que nuestro test se ejecute.

Verificación: Finalmente, obtenemos el resultado de la “ejecución” y lo comparamos con el valor que se supone que debería entregar.

Veamos algo de codigo sencillo en C# que es muy facil de leer.

public void Test_Cuando_El_Email_Es_Invalido___Debo_Avisar_Email_Invalido() {
// Preparacion
var componente = New ComponenteDestinatarios();
// Ejecucion
var resultado = componente.Buscar("soy un email invalido");
// Verificacion
var valorEsperado = "El email 'soy un email invalido' es invalido";
if (resultadoObtenido == valorEsperado)
console.log("El test funciono bien")
}else{
console.log("El test fallo");
}
}

Nota: La verificacion, en c#, se realiza con el objeto assert. En este ejemplo imprimimos en consola para no agregar mas complejidad y que el test sea mas facil de entender.

Escribiendo código productivo que vuelva nuestro RED en GREEN.

¡Genial! Tenemos nuestro primer test escrito que da ROJO. El siguiente paso es escribir código productivo que convierta ese test en VERDE. Para ello deberemos tener en cuenta las siguientes premisas.

  • Escribir SOLO lo necesario para que nuestro test se convierta en VERDE.
  • No buscar código perfecto ni performante, ya haremos eso en el REFACTOR.
  • La solución debe ser la opción más simple y sencilla. ¿Separar una responsabilidad en otro componente? NO! ¿Eliminar código repetido? ¡NO!. Ya haremos eso en el REFACTOR.
public class ComponenteDestinatarios {
public string Buscar(string email) {
if(email.IndexOf('@') < 0 && email.IndexOf('.') < 0)
return "El email 'soy un email invalido' es invalido";
return "";
}
}

¿Es esto un código aceptable? Para un entorno productivo no, pero para un test unitario si. Nuestro test unitario ahora está en VERDE.

Analicemos qué logramos con esta implementacion.

  • Creamos una clase llamada ComponenteDestinatarios.
  • Detectamos la necesidad de un método Buscar, decidimos enviar y recibir un string. Posiblemente lo mejor hubiera sido enviar un string y esperar una excepción con el error pero no quise agregar complejidad al ejemplo.
  • Decidimos que nuestro criterio de un email válido tenga al menos una @ y un punto.

Los tres logros conseguidos fueron decisiones de diseño, todos ellos tomados durante la escritura de un test y su código funcional. De esto se trata TDD, pequeñas decisiones de diseño que nos llevan a una solución mas grande.

EJERCICIO 2: Escribir al menos un Test unitario mas (ver lista del ejercicio 1) y escribir su logica para que funcione.

Pasando del VERDE a un codigo terminado.

Cuando finalmente tenemos nuestro (o nuestros) test unitario en verde, es momento de mejorar el código. Hay un par de cuestiones a tener en cuenta.

  • Todo nuestro código puede ser reescrito ya que tenemos un test unitarios (o varios) que están esperando que se cumpla una condición. No tengas miedo de borrar y escribir todo de nuevo!
  • Aquí sí importa la performance, las buenas prácticas y la claridad. Es el momento de dejar todo bonito.
  • También es momento de buscar otras opciones de diseño. Recordemos que TDD es una herramienta de diseño.

A este refactor se me ocurren tres posibles soluciones. Pueden ser más, estas tres son las que se me ocurrieron a mi en el momento.

Opción 1: Inyectando un componente de validación a la clase.

public class ComponenteDestinatarios {
private IValidadorEmail _validadorEmail;

public ComponenteDestinatarios(IValidadorEmail validador) {
_validadorEmail = validador;
}
public string Buscar(string email) {
if (!this._validadorEmail.IsValid(email)) {
return "El email 'soy un email invalido' es invalido";
}
return "";
}
}

El primer test que escribí ya me está indicando que el plan que tenía originalmente no era muy bueno, ¿Por que? Porque tenía un componente con dos responsabilidades, validar el email y buscar el destinatario.

Entonces, separamos las responsabilidades en dos componentes diferentes y realizamos las pruebas del validador de email más adelante.

Opción 2: Moviendo la complejidad del IF a un método privado

public class ComponenteDestinatarios {
public string Buscar(string email) {
if(this.EmailIsInvalid(email))
return "El email 'soy un email invalido' es invalido";
return "";
}
private bool EmailIsInvalid(string email) {
return email.IndexOf('@') < 0 && email.IndexOf('.') < 0
}
}

Es un arreglo provisorio pero también válido ya que me permite continuar avanzando con el diseño del componente sin detenerme en separar las responsabilidades. Posiblemente esta validación de mail sea la única que hagamos y tal vez no es tan necesario tener un validador inyectado. Lo sabremos cuando escribamos más tests.

Opción 3: Separamos el IF en dos sentencias diferentes

public class ComponenteDestinatarios {
public string Buscar(string email) {
if(email.IndexOf('@') < 0)
return "El email 'soy un email invalido' es invalido";
if(email.IndexOf('.') < 0)
return "El email 'soy un email invalido' es invalido";
return "";
}
}

Es el mismo código pero le bajamos un poco la complejidad separando el IF en dos sentencias. Es fácil de leer pero se puede mejorar.

Recordemos que siempre hay oportunidades de mejorar y que posiblemente volvamos a pasar por aquí. Sabiendo eso podemos acomodar solo una línea (como en este caso) y continuar… después de todo “es mejor terminado que perfecto”. Cuando tenemos un ciclo terminado, debemos comenzar de nuevo escribiendo un nuevo test y cumpliendo los pasos que ya hicimos.

A partir de aca solo queda sentarse y empezar a ganar experiencia, exis.

Los principios FIRST nos ayudaran con las buenas practicas al escribir test unitarios.

Mocks y Fakes ¿Debo ir a la base de datos para escribir mis test?

Este repositorio de un proyecto de pruebas en net core hecho con TDD. Aca tenes un ABM de estudiantes y algunos casos de uso para que pruebes escribir test y código productivo por tu cuenta.

Por aqui un video que te puede ayudar a entender un poco mejor lo que es TDD

--

--