TDD
Desarrollo guiado
por pruebas de software
INTEGRANTES:
ALVARO PINILLA
EDUARDO LOPEZ
ALEXIS MIRANDA
CURSO:
GESTION DE CALIDAD
DE SOFTWARE
LABORATORIO
PROFESOR:
ERWIN FISCHER
SEPTIEMBRE 2013
Contenido
1. Introducción.
En el siguiente informe se investigó sobre el desarrollo
guiado por pruebas de software, TDD (Test-driven development). Se explicaran
los fundamentos principales de esta técnica, así como la bibliografía que hay
sobre este tema, los principales libros y autores. Por otra parte se mostrará,
con un ejemplo, como es la aplicación de esta práctica de programación.
2. ¿Qué
es TDD?
El Desarrollo Dirigido por Tests
(Test Driven Development), conocido como TDD, es una técnica de diseño e
implementación de software incluida dentro de la metodología XP. TDD es una
técnica para diseñar software que se centra en tres pilares fundamentales:
- La implementación
de las funciones justas que el cliente necesita y no más.
- La minimización del
número de defectos que llegan al software en fase de producción.
- La producción de
software modular, altamente reutilizable y preparado para el cambio.
TDD es realmente una herramienta
de diseño que convierte al programador en un desarrollador. TDD es la respuesta
a las grandes preguntas de:
¿Cómo lo hago?, ¿Por dónde
empiezo?, ¿Cómo sé qué es lo que hay que implementar y lo que no?, ¿Cómo
escribir un código que se pueda modificar sin romper funcionalidad existente?
No se trata de escribir pruebas
como locos, sino de diseñar adecuadamente según los requisitos.
3. El
algoritmo TDD.
La esencia de
TDD es sencilla pero ponerla en práctica correctamente es cuestión de
entrenamiento, como tantas otras cosas.
El algoritmo TDD
sólo tiene tres pasos:
·
Escribir
la especificación del requisito (el ejemplo, el test).
·
Implementar
el código según dicho ejemplo.
·
Refactorizar
para eliminar duplicidad y hacer mejoras
De esta forma el
ciclo de desarrollo conducido por pruebas queda definido de la siguiente forma:
Elegir un requisito: Se elige de una lista el requerimiento que se cree que nos dará mayor
conocimiento del problema y que a la vez sea fácilmente implementable.
Escribir una prueba: Se comienza escribiendo una prueba para el requisito. Para ello el
programador debe entender claramente las especificaciones y los requisitos de
la funcionalidad que está por implementar. Este paso fuerza al programador a
tomar la perspectiva de un cliente considerando el código a través de sus interfaces.
Verificar que la prueba falla: Si la prueba no falla es porque el requerimiento
ya estaba implementado o porque la prueba es errónea.
Escribir la implementación: Escribir el código más sencillo que haga que la
prueba funcione. Se usa la metáfora "Déjelo simple" ("Keep It
Simple, Stupid" (KISS)).
Ejecutar las pruebas automatizadas: Verificar si todo el conjunto de pruebas
funciona correctamente.
Eliminación de duplicación: El paso final es la refactorización, que se utilizará
principalmente para eliminar código duplicado. Se hacen de a
una vez un pequeño cambio y luego se corren las pruebas hasta que funcionen.
Actualización de la lista de requisitos: Se actualiza la lista de
requisitos tachando el requisito implementado. Asimismo se agregan requisitos
que se hayan visto como necesarios durante este ciclo y se agregan
requerimientos de diseño (P. ej que una funcionalidad esté desacoplada de
otra).
Tener
un único repositorio universal de pruebas facilita complementar TDD con otra
práctica recomendada por los procesos
ágiles de desarrollo, la "Integración Frecuente". Integrar frecuentemente
nuestro trabajo con el del resto del equipo de desarrollo permite ejecutar toda
batería de pruebas y así descubrir si nuestra última versión es compatible con
el resto del sistema. Es recomendable y menos costoso corregir pequeños
problemas cada pocas horas que enfrentarse a problemas enormes cerca de la
fecha de entrega fijada.
4.
Principales
autores y libros sobre TDD.
Las primeras páginas del libro de Kent Beck (uno de los padres de la metodología XP) dan unos argumentos muy
claros y directos sobre por qué
es beneficioso convertirla en nuestra herramienta de diseño principal. Estas son algunas de las razones que da Kent junto con
otras destacadas figuras de la industria:
·
La
calidad del software aumenta.
·
Conseguimos
código altamente reutilizable.
·
El
trabajo en equipo se hace más fácil, une a las personas.
·
Nos
permite confiar en nuestros compañeros aunque tengan menos experiencia.
·
Multiplica
la comunicación entre los miembros del equipo.
·
Las
personas encargadas de la garantía de calidad adquieren un rol más inteligente
e interesante.
·
Escribir
el ejemplo (test) antes que el código nos obliga a escribir el mínimo de
funcionalidad necesaria, evitando sobre diseñar.
·
Cuando
revisamos un proyecto desarrollado mediante TDD, nos damos cuenta de que los
tests son la mejor documentación técnica que podemos consultar a la hora
de entender qué misión cumple cada pieza del puzzle.
Frecuentemente,
nos encontramos con gente muy desconfiada que mira con lupa el código de su
equipo antes de que nadie pueda hacer “commit” al sistema de control de
versiones. Esto se convierte en un cuello de botella porque hay varias personas
esperando por el jefe (el arquitecto) para que dé el visto bueno y a este se le
acumula el trabajo. Cuando el jefe sabe que su equipo hace TDD correctamente
puede confiar en ellos y en lo que diga el sistema de integración contínua y las
estadísticas del repositorio de código.
Para el
programador junior se convierte en su guía que le indica que paso tiene que dar
ahora. Y así, un paso tras otro, le guía en la implementación de la tarea que
le ha sido asignada.
El libro “Diseño Ágil con TDD” de los autores Carlos Blé
Jurado, Juan Gutiérrez Plaza, Fran Reyes Perdomo y Gregorio Mena, Se puede descargar en www.dirigidoportests.com/el-libro, donde también hay enlaces para
quien quiera comprar la versión impresa o leerlo online. Es uno de los libros más
completos que pudimos encontrar sobre lo que es TDD, incluye temas como que
entendemos como agilísimo, los tipos de test y su importancia, y varios
ejercicios prácticos.
5. Ejemplo
práctico.
Explicando el problema.
Bien, el problema que se nos plantea es la creación de un
programa que calcule el importe de una colección de libros aplicando un
determinado descuento en función de los distintos tipos de
libros que compremos. Partiendo de que cada libro tiene un coste de
8€ el cliente nos indica que, si
se adquieren dos libros distintos, se aplique un 5% de descuento a ambos libros,
un 10% si se compran 3, un 20% para 4 distintos y un 25% para la serie completa
de 5 libros.
Otra condición del cliente es que el programa obtenga el precio más económico
si hay varias combinaciones posibles de packs.
Como entrada recibiremos un array con los códigos de los
libros a comprar (de 0 a 4) y como salida obtendremos un valor double.
¡Muy bien! Ya sabemos que tenemos que hacer y que quiere
el cliente por lo que vamos a escribir las primeras pruebas.
Pruebas Sencillas
Lo primero que debemos hacer es escribir unas pruebas sencillas que
implementen los casos más básicos. La primera y más sencilla es
que cuando nuestra cesta de compra esté vacia el precio total es 0. En NUnit sería así:
[Test]
public void
EmptyBasket()
{
Assert.AreEqual(0,
_bookPriceCalculator.CalculateBasket(new int[] { }));
}
Sin entrar en explicar la sintaxis de NUnit simplemente
indicar que esta prueba comprueba que si se le pasa un array vacio el sistema
devuelve 0.
¿Cómo resolvemos esta prueba
con la menor cantidad de código posible?
Muy fácil:
public double CalculateBasket(int[] books)
{
return 0;
}
Esa es la solución más sencilla que hace que la prueba
sea correcta. Obviamente en cuanto añadamos una segunda prueba tendremos que
rehacer nuestro código pero de esto se trata TDD.
¡Ok! Vamos ha añadir una nueva prueba para indicar una
nueva condición impuesta por el cliente: Cada libro vale 8€
[Test]
public void
TwoSameBook()
{
Assert.AreEqual(8 * 2,
_bookPriceCalculator.CalculateBasket(new int[] { 0, 0 }));
}
En esta prueba indicamos que si se compran dos veces el
libro "0" el precio total es de 16€.
¿Cómo modificamos nuestro
método para que pase las dos pruebas?
public double
CalculateBasket(int[] books)
{
return
books.Length * 8;
}
¡Vale! No es muy elegante pero solucionaría cualquier
prueba que no implique un descuento por lo que podemos cerrar el capítulo de
pruebas sencillas.
Añadiendo Funcionalidad: Calculando Descuentos
Nuestro cliente nos había puesto como premisa que, si se
compraban dos o más libros distintos había que aplicar un descuento determinado
por lo que vamos a crear una serie de pruebas de ejemplo que implementen esta
funcionalidad:
[Test]
public void
TwoDifferentBooks()
{
Assert.AreEqual(8 * 2 * 0.95,
_bookPriceCalculator.CalculateBasket(new int[] { 0, 1 }));
}
[Test]
public void
SeveralDiscountsTwo_One()
{
Assert.AreEqual(8 + (8 * 2 * 0.95),
_bookPriceCalculator.CalculateBasket(new int[] { 0, 0, 1 }));
}
Como veis en las pruebas tan solo declaramos una entrada
de datos y la solución que nuestro cliente espera. Es trabajo del desarrollador
el encontrar el modo en que el código del programa
satisfaga todas las nuevas pruebas sin "romper" ninguna de las
anteriores ya implementadas.
La solución que se me ha ocurrido a mi es hacer una clase
"BookPack" que simbolice un "paquete de libros" donde todos
los libros deben de ser distintos por lo que cada compra se traduce realmente
en varios "BookPacks" cada uno con su precio y descuento.
Sin tener en cuenta la última condición (que indicaba que
se ha de escoger la combinación más económica), la solución más sencilla es
esta:
List<BookPack>
BookPacks = new List<BookPack>();
foreach (int
bookCode in books)
{
BookPack currentBookPack = null;
foreach (var bookPack in BookPacks)
{
if (!bookPack.HasThisBook(bookCode))
{
currentBookPack = bookPack;
}
}
if (currentBookPack == null)
{
BookPacks.Add(new
BookPack(bookCode));
}
else
{
currentBookPack
.Books.Add(bookCode);
}
}
return
BookPacks.Sum(bookPack => BookPack.GetBookPackPrice(bookPack.Books.Count));
Donde "GetBookPackPrice" devuelve el precio del
paquete en función de cuantos libros tiene:
public static
double GetBookPackPrice(int quantity)
{
return
8 * quantity * GetDiscountPerDifferentBooks(quantity);
}
//GetDiscountPerDifferentBooks devuelve 1, 0.95, 0.9, 0.8
o 0.75 en función de si se compran 1, 2, 3, 4 o 5 libros distintos
respectivamente.
Con esto ya tenemos implementada tanto la funcionalidad
básica (ningún libro o varios libros iguales) como el descuento básico por
"paquetes de libros". Sólo nos faltaría añadir la última condición
del cliente: que se elija la combinación más económica a la hora de hacer los
paquetes.
Pruebas Finales: Obteniendo el paquete más económico
No es lo mismo hacer dos paquetes de 4 libros (51.2€ en
total) que 1 de 5 y otro de 3 (51.6€). Con el código anterior obtendríamos la
opción más desfavorable ya que solo se crea un nuevo paquete cuando el libro
actual ya se encuentra en el paquete actual y no se tiene en cuenta el importe
final.
Para añadir esta nueva funcionalidad vamos a escribir la
última prueba:
[Test]
public void SeveralDiscountsFourFour()
{
Assert.AreEqual(2 * (8 * 4 * 0.8),
_bookPriceCalculator.CalculateBasket(new int[] { 0, 0, 1, 1, 2, 2, 3, 4 }));
}
La implementación para este caso consiste en cambiar el
concepto "CurrentBookPack" que tan solo va rellenando paquetes por
"CheapestBookPack" el cual compara entre los distintos paquetes y se
queda con el más económico.
Como el código resultante ya
es más se puede ver directamente en el repositorio de GitHub
donde está guardado.
6. Conclusiones.
Creo que este es un gran ejemplo para explicar las virtudes de TDD ya
que en un mismo proyecto hemos
tenido que hacer 3 modificaciones que han alterado por completo el
funcionamiento de la aplicación.
Al principio solo teníamos que calcular importes sencillos (8 *
unidades), después se añadió unos descuentos por volumen para terminar con un
sistema más inteligente que se queda con la combinación más económica.
Ahora
extrapola este ejemplo a un proyecto real con cientos de clases y miles de
líneas y piensa que pasaría si tuvieras que hacer este tipo de
cambios... asusta, ¿verdad?
Y es que ¿cuántas veces hemos "arreglado
algo y roto otro cosa"? ¿Cuantas veces los requerimientos del software cambian
y debemos adaptarnos rápidamente
a las nuevas necesidades? ¿Cuantas veces programamos más de lo debido?
TDD ayuda a
solucionar estos problemas facilitándote el modo en el que organizas tus tareas.
Cuando programas primero una prueba estás abstrayéndote de algoritmos y
estrategias para centrarte en entender el problema y la solución exacta que
quiere el cliente.
Si logras crear pruebas simples que explican una pequeña parte del
código podrás tener un sistema casi perfecto cuando el producto se entregue
además de ganar en flexibilidad en caso de los requerimientos cambien en medio
del desarrollo.
Está claro que escribir
pruebas antes de programar es un trabajo extra que en muchos
casos no se puede asumir pero, a la
larga, ahorra mucho tiempo en mantenimiento y evita problemas de código heredado.
TDD, como
otras herramientas dentro del agilísimo, solo
te da una serie de "buenas
practicas" que te ayudan a mejorar tu calidad como
desarrollador. Está en
tu mano decidir hasta qué punto utilizar TDD