Table of Contents
Continuamos con nuestra serie de artículos sobre TDD. En la primera parte hablamos sobre la teoría que hay detrás de TDD y unit testing. En esta segunda parte, TDD Primer Ciclo, ya empezamos a desarrollar nuestra aplicación, desarrollaremos una aplicación de notas donde un usuario podrá escribir notas y todo lo que se nos vaya ocurriendo. Dejad en comentarios si estáis interesados en ver cómo desarrollamos alguna funcionalidad concreta en nuestra aplicación. Empecemos con TDD primer ciclo!
TDD Primer Ciclo
Para empezar a desarrollar nuestra aplicación, podríamos empezar con la entidad usuario (bastante genérica y personalmente me parece hasta fea, se usa para todo), veremos más adelante si hace falta cambiarla a algo más concreto.
Para empezar tenemos que tener en mente en todo momento el ciclo de TDD, el primer paso es RED, por lo cual buscamos un único test que falle pero qué, una vez lo pongamos en verde, tengamos una funcionalidad de nuestro aplicación (parcial o completa).
En este proceso tenemos que «inventarnos» una posible interfaz de nuestra entidad. Ésta será evolucionada porque se nos irán ocurriendo cosas nuevas o mejores y es probable que tengamos que modificar tanto el test como el código de producción. No debemos pensar que una vez picado un test y puesto en verde, éste no puede ser modificado o incluso eliminado… Vamos empezar con un test que verifique que un usuario se ha creado con un nombre y apellido, por ejemplo:
describe("given user", () => {
describe("with valid data", () => {
test("is created", (done: any) => {
const validUser = new User("someId", "Oscar", "Galindo");
expect(validUser.toString()).toEqual("User(someId,Oscar,Galindo)");
done();
});
});
});
En este primer test comprobamos con un toString (*) que el usuario creado es el que esperamos, hacer un test que compruebe todas las propiedades de una entidad puede llegar a tener un coste a corto plazo a medida que la entidad crezca, podemos preguntarnos si tiene sentido testear que todas las propiedades de la entidad son las que deberían. (The Art Of Unit Testing).
(*) El toString no lo hemos testeado (explícitamente) ya que es un método que usamos para hacer debug/test.
Muy bien, ya tenemos un test en rojo como nos dice TDD primer ciclo: RED. Volvamos a pensar en el TDD primer ciclo, tenemos que poner ese test en verde de la manera más simple posible y por absurdo que parezca, es lo que hay que hacer. TDD te ayuda a hacer foco en una única cosa pero eso es algo que depende del programador, de ahí que tenemos que tener en mente los procesos que existen y nos ayudan.
El primer error es al compilar, no sabe que es «User», cuando lo tengamos creado deberíamos asignarle a las propiedades de la clase los parámetros de entrada:
export class User {
constructor(private id: string, private name: string, private lastname: string) {}
}
En los ejemplos que se ven por internet, la implementación que suelen hacer como primera iteración es hardcodear lo máximo posible, falsear la respuesta para poner el test en verde… bien, esto no es 100% así.
La mayoría de developers que quieren aprender TDD ven de primeras esto como una perdida de tiempo, puede parecer absurdo hardcodear la respuesta cuando conocemos la implementación (la suficiente para poner el test en verde) y es simple, bien, pues, es aceptado también dentro de TDD. Existen varias maneras de poner el test en verde de la manera más rápida posible aunque en este capítulo sólo voy a mencionar dos de ellas:
- Fake it: Falsear la respuesta con tal de poner el test en verde lo mas rápido posible
- Obvious implementation: Si conocemos la implementación, es fácil de desarrollar y hay poco riesgo de no poner el test en verde, adelante con ello.
- A tener en cuenta es que si no ponemos el test en verde de primeras, volver a Fake it. Lo importante aquí es hacer foco en lo que nos pide el test, nada mas.
Genial, ya estamos en GREEN, ahora viene la parte del refactor. Esta parte es la gran olvidada, he llegado a escuchar algo como «tdd te ayuda hacer código de mierda que funciona», cuesta entender (y es parte del aprendizaje de TDD) que TDD no es hacer tests. TDD, como ya hemos comentado, es un proceso que te ayuda a diseñar, a modelar tu código dando pasos lo más pequeños posibles con seguridad, confirmar que lo que tenemos en nuestra cabeza es correcto y darnos un espacio continuo de diseño. Si te saltas este último paso no estas haciendo TDD, estas haciendo TF (Test First). La fórmula de TDD (no exacta y discutible pero se entiende): TF + Refactor = TDD
Dicho esto, qué clase de Refactor podemos hacer en nuestro primer test? En este caso no haremos nada, no tenemos ningún tipo de duplicación y no queremos abstraernos nada. Voy hacer referencia a un artículo muy bueno donde te pone una checklist a tener en cuenta en cada paso del ciclo: Checklist TDD y también a un gran libro Refactoring.
Sigamos, ya tenemos el TDD primer ciclo terminado. En base al primer test, me he preguntado si un usuario puede existir con un nombre vacío, también me pregunto si puede tener números o más de un millón de caracteres, pero me centro en una y las otras las voy anotadas en una ToDo list para ir paso a paso (suelo tener un ToDo.md dentro de la app donde voy anotando todo lo que voy viendo), obviamente son validaciones que debemos hacer y vamos a hacer.
Primero, como siempre, creamos un test que valide que un nombre no puede estar vacío, fácil no?
describe("with empty name", () => {
test("should throw", (done: any) => {
const userThrowWithEmptyNameCreator = () => new User("someId", "", "Galindo");
expect(userThrowWithEmptyNameCreator).toThrow();
done();
});
});
Hemos conseguido un test que falla ante un nombre vacío, esta vez nos dice que debería lanzar una excepción pero no lo está haciendo, algo que es normal, no esta implementado. Vamos a desarrollar de la manera más simple esta funcionalidad:
export class User {
constructor(private name: string, private lastname: string) {
if (name === "") {
throw new Error("empty name");
}
}
}
Volvemos a green! Hemos avanzado, tenemos nueva funcionalidad, tenemos un entidad usuario con ciertas reglas de validación implementadas y lo bueno es que nos da cierta libertad en hacer ciertos refactors con seguridad, ya que tenemos test sobre un código ya validado que nos informará si rompemos algo.
Una vez tenemos green, tenemos que pasar al refactor de lo que tenemos y la verdad, a nivel de código de producción no vamos a tocar nada pero, y el código de test? ese también es nuestro código, ese código también cuenta y también se tiene que mantener. Puede ser preocupante tener un código de producción difícil de mantener/leer pero es peor si ese tipo de código se encuentra en los tests. Si te encuentras este tipo de código en los tests pasan dos cosas:
- Dejas de confiar en tus tests
- No les haces caso, empiezan a aparecer skipped tests, comentados o directamente eliminados hasta que eventualmente no hay tests, y ya sabemos que perdemos con esto.
Que podemos refactorizar en el poco test que tenemos? en los dos tests creamos un usuario, podemos sacar esa creación a un método quedándonos algo así:
describe("given user", () => {
const createUser = (name: string, lastname: string) => new User("someId", name, lastname);
const createValidUser = () => createUser("Oscar", "Galindo");
const createUserWithEmptyName = () => createUser("", "Galindo");
describe("with valid data", () => {
test("is created", (done: any) => {
const validUser = new User("someId", "Oscar", "Galindo");
expect(validUser.toString()).toEqual("User(someId,Oscar,Galindo)");
done();
});
});
describe("with empty name", () => {
test("should throw", (done: any) => {
expect(createUserWithEmptyName).toThrow();
done();
});
});
});
Existen muchas estrategias para la creación de estos objetos:
- La que hemos usado, crear un método en el test de creación de entidad, pero si esa entidad crece podemos llegar a tener problemas (aunque creo que podríamos mirar si es un problema de diseño)
- Object Mother
- Test Data Builder
Una vez refactorizado podemos ver como los tests siguen en verde, hemos ganado mucha legibilidad y hemos facilitado a nuestro futuro o al del otro desarrollador la tarea de mantenimiento (y encima ayudamos a entender a los demás como funciona la aplicación, de ahí que los tests sean parte de la documentación de un proyecto)
Ahora vamos hacer exactamente lo mismo con lastname. Empezamos por el test, será prácticamente igual que el de name, tenemos que seguir con el refactor previo por eso, ya tenemos un factory para crear users, porqué no usarlo?
const createUserWithEmptyLastName = () => createUser("Oscar", "");
describe("with empty lastname", () => {
test("should throw", (done: any) => {
expect(createUserWithEmptyLastName).toThrow();
done();
});
});
Test en rojo, genial, aquí empieza donde el desarrollador empieza a perder un poco el foco y vuelvo a repetir una de las 3 leyes de TDD y es que no picar más código que el suficiente para hacer pasar el test, cual es ese código?:
export class User {
constructor(private name: string, private lastname: string) {
if (name === "") {
throw new Error("empty name");
}
if (lastname === "") {
throw new Error("empty lastname");
}
}
}
Ya tenemos el test en verde y nueva funcionalidad! Precioso, ahora viene algo divertido, refactor! y esta vez en código de producción 🙂 Aquí hay muchas técnicas de abstracción (usaremos ValueObjects) para no duplicar que veremos en la siguiente parte.
Y no te olvides de suscribirte a nuestro boletín mensual para recibir más información sobre TDD primer ciclo.
Si te gustó este artículo sobre TDD primer ciclo, te puede gustar:
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
Sobre Dioses y procrastinación
Arquitectura de microservicios
Simular respuestas del servidor con Nodejs
Mapa de los “main players”: ecosistema startup y tech en Barcelona
Ecosistema de salud digital en Barcelona
Author
-
Software developer with over 16 years experience working as Fullstack Developer & Backend Developer.
Ver todas las entradas