TDD en entornos .net #bdc11

21 Nov 2011 · 5 mins. de lectura

La semana pasada, los días 17, 18 y 19, estuvimos en la: Barcelona Developers Conference. El primer día por la mañana tuve el placer de, además de escuchar grandes ponencias, dar una charla sobre TDD en entornos .net. Y de coletilla, para el mundo real. Esta última frase que hacía del título algo parecido a una película mala de antena 3 por la tarde, venía a raíz de que mucha gente habla de TDD, y casi nadie de cómo se aplica realmente en el desarrollo diario.

Durante poco más de una hora intenté que los asistentes entendieran el por qué de esta metodología, cómo nos beneficia y cómo aplicarla en el desarrollo diario.

Si no pudisteis asistir, os diré que TDD son las siglas de Test Driven Development o en castellano: desarrollo guiado por las pruebas. Y es una técnica de desarrollo (de eXtreme Programing) que se basa en dos pasos: primero escribir la pruebas y después refactorizar.

test driven

La programación extrema engloba una serie de metodologías de programación ágiles, basadas en que a lo largo de un desarrollo van a ocurrir cambios de especificaciones y en lugar de intentar prevenir esto con una gran cantidad de código, que en ocasiones sobra, se decide adaptarse a esos cambios en cualquier fase del ciclo de vida del proyecto.

Sobre refactorización, es una técnica de la ingeniería del software que reestructura el código fuente sin cambiar su funcionalidad. Es decir, cambiar nombres de variables y métodos; reordenar el código; dividir una función en varias, … Algo que se conoce comúnmente como limpiar el código.

Las pruebas se escriben generalmente como pruebas unitarias (o unit tests en inglés). Una prueba unitaria es una forma de probar que un determinado bloque de código fuente, funciona correctamente. Es decir, mediante un pequeño programa comprobamos que cada porción de código fuente escrita funciona correctamente por separado. Y aunque estas pruebas no nos garantizan el funcionamiento global de la aplicación, pueden ser completadas con otro tipo de tests como los de integración, que comprueban que los diferentes artefactos funcionan correctamente unos con otros.

Para que se considere una prueba unitaria válida esta debe ser:

Y como en mucho proyectos se tiende a olvidar este punto, lo repetimos ;) : una prueba unitaria es parte del código de la aplicación.

La estructura de un test unitario es simple:

Triple A como los videojuegos buenos…

Pero vamos a volver al concepto inicial: desarrollo guiado por las pruebas.

Generalmente expresado como Red -> Green -> Refactor, expone un diagrama de actividad simplificado de esta técnica.

Quiere decir que primero vamos a hacer un test unitario, comprobaremos que falla (Red), se completa el código de la forma más simple solo para que este pase (Green), y una vez funciona se refactoriza el código para así limpiarlo de malos nombres, espacios de nombre confusos  o de código repetitivo…

Pero hemos dicho que esta es la versión simple…

En la versión real nosotros nos encontraríamos dentro de un proyecto. Como estaremos usando metodologías ágiles (algo como scrum), tendremos un product backlog para el sprint actual. Dentro de este encontraremos las historias de usuario que nos hablan de un requerimiento del software que estamos desarrollando. Y este requerimiento lo dividiremos en una o varias especificaciones.

Una vez tenemos claro lo que tenemos que probar (la especificación) realizamos nuestro test unitario para acto seguido comprobar que este falla. Es muy importante comprobar esto, porque si no falla de buenas a primeras es porque nuestro test es posible que no compruebe nada en realidad.

Una vez tenemos nuestro test fallando podemos ponernos a codificar la mínima cantidad de código necesaria para que se vea cumplido. O seguir el principio KISS (Keep It Simple, Stupid!).

El siguiente paso sería comprobar que todas las pruebas que tenemos escritas pasan. Es decir, no solo la actual si no todas las demás porque podemos haber roto algo.

A partir de aquí se nos permitirá hacer una refactorización. Hay que ser pragmáticos y modificar el código, pero no su comportamiento. Y aplicar el principio DRY (Don’t Repeat Yourself). Entonces volvemos a comprobar de nuevo que todos los tests pasany pasamos a la siguiente especificación.

Me gustaría poneros un ejemplo (en la #bdc11 si lo hicimos, aunque muy rápido). Aunque creo que esto queda muy claro en vivo, leyendo es posible que no aclare demasiado… Así que explicaré cómo TDD nos llevó a crear clases y propiedades. Como tuvimos que desacoplar artefactos usando interfaces y al final nos decidimos a usar inyección de dependencias. Pero solo cuando el propio test nos llevó a ello.

Para terminar con este enorme post comentar los beneficios de esta técnica:

Esto que vemos es una curva de coste. Se suele utilizar para representar el coste de desarrollo de una aplicación en dependencia de la metodología utilizada. Muy común cuando la temática de la charla es sobre deuda técnica.

En el eje vertical vemos el coste y en el horizontal el tiempo. La línea naranja es el desarrollo usando test driven development y la roja el método “tradicional” de codificar.

La línea roja empieza a tener resultados muy rápidamente, pero el coste de desarrollo se va haciendo más grande con el tiempo. Al final existe una cantidad enorme de código sin ningún test que lo compruebe. Y el más mínimo cambio tiene un impacto en el coste muy grande.

Por otro lado vemos en un principio un desarrollo TDD es más costoso y tardará más tener resultados semejantes al otro. Eso sin contar con el tiempo de aprendizaje del equipo. Pero la curva se estabiliza rápidamente. Y una vez estabilizada y con una serie de test de respaldo, las modificaciones no se hacen a ciegas, y además son más simples, ya que esta técnica nos ha hecho seguir otra serie de principios de desarrollo como:

YAGNI: You Ain’t Gonna Need It. TDD nos ayuda a prevenir la escritura de código que no va a ser utilizado en nuestra aplicación. Básicamente porque solo escribimos lo mínimo necesario para cubrir las especificaciones.

Menos uso del debugger: se dice que los buenos programadores de TDD dejan de usar el debugger, ya que no lo necesitan. Todo su código ya está probado. ;P

Interface Segregation Principle: es un principio SOLID (la ‘i’ concretamente) que dice que debemos segregar las responsabilidades en interfaces. TDD nos obliga a separar componente según responsabilidad porque los test unitarios como norma, no pueden comprobar más de una funcionalidad en un solo artefacto (unitario).

Dependency Injection: el patrón de inyección de dependencias dice que deberíamos crear un contendor que sea quien controle y resuelva las dependencias de todos los artefactos de nuestra aplicación. Y seguiremos este patrón para facilitarnos la creación de objetos mediante interfaces correctamente segregadas.

- Y estos dos últimos punto nos llevan a seguir otro principio de SOLID: Dependency Inversion Principle. Que dice que una clase debería de depender de las abstracciones y no al contrario.

- Desarrollaremos siguiendo los principios ágiles: dividimos el problema en pequeños pasos y lo vamos solucionando uno a uno. Buscaremos especificaciones y las codificaremos una a una con su test unitario. Nos adaptaremos a los cambios rápidamente.

Y hasta aquí puedo escribir, debajo encontrareis el archivo con la presentación de la ponencia…

buy me a beer