Anteriormente hemos dicho que el eXtreme Programming (XP) propone un modelo de pruebas ligeramente distinto. Bien, expliquemos esto con detalle porque merece la pena.
Una de las metodologías ágiles existentes es la llamada
Test Driven Development, (o TDD) es decir: desarrollo guiado por pruebas. Esta metodología, (una de las que inspiró a Kent Beck a la hora de concebir el XP) propone realizar las pruebas unitarias antes que la propia unidad.
Espera un momento... ¿cómo vamos a probar algo que todavía no existe? Es como si intentamos medir la velocidad máxima de un coche antes de que se haya fabricado el primer ejemplar. Pues aunque suene raro, y no sea posible realizar en otros ámbitos, en el mundo del desarrollo de software esto es viable, y además muy recomendable.
Esta metodología está muy pautada y hay que seguir los siguientes pasos:
- Pensar en la unidad o funcionalidad que queremos desarrollar, centrándonos en cómo nos gustaría que se usase desde el exterior. Una buena manera de hacer esto es imaginarnos que vamos a comprar esa unidad a una empresa y podemos definir cómo queremos usarla. El cliente siempre manda ¿no? pues imaginemos que somos los clientes y van a programarnos esa unidad a nuestro gusto.
- Escribir el pseudocódigo de uno o varios ejemplos de su uso más habitual. Cada uno de estos ejemplos se llama en análisis "Caso de Uso", ya que define uno de los casos en que se usará la unidad que estamos analizando. No detallaremos ni escribiremos ejemplos de usos extraños, sino los ejemplos típicos de uso. Como vamos a ser los que usemos la futura unidad, intentaremos que el uso sea lo más sencillo posible a la vez que flexible y potente. Cuando terminemos este paso, tendremos una lista de Casos de Uso, además de una lista de tareas a completar. De este modo, como dicen en mi pueblo, hemos matado dos pájaros de un tiro: hemos obtenido el análisis de casos de uso (incluso podríamos representarlo con un diagrama UML de Casos de Uso), y hemos confeccionado una lista de tareas a completar para dar por finalizada la unidad.
- Codificar el pseudocódigo de cada uno de los ejemplos en forma de Caso de Prueba, verificando en todo momento los retornos de los métodos que hemos definido en el paso anterior. De este modo, cada Caso de Uso tiene su correspondiente Caso de Prueba que lo verifica. Durante esta codificación refinaremos el uso que hemos imaginado, añadiendo o quitando parámetros, modificando los retornos de los métodos, etc.
- Compilar ese Caso de Prueba. Lógicamente, fallará la compilación, ya que se están haciendo llamadas a métodos o unidades que no existen.
- Codificar todas aquellas clases/funciones/métodos/lo-que-sea para que la prueba compile. Es importante que todos los métodos que codifiquemos retornen un valor de error, que dependiendo del método en cuestión puede ser false, -1, o cualquier otra cosa.
- Compilar otra vez el Caso de Prueba: ahora tiene que compilar correctamente ya que en el paso anterior hemos añadido todo lo necesario.
- Ejecutar la prueba: fallará estrepitosamente porque todos los métodos o funciones están vacíos y retornan un valor de error. Si alguna prueba pasa correctamente, significará que la prueba está mal escrita.
- Codificar cada una de las funciones/métodos para que todas las pruebas vayan pasando. Posiblemente no pasarán a la primera, sino que las pruebas nos irán indicando si el código que escribimos va por buen camino o no.
- Daremos por finalizado el ciclo cuando todas las pruebas hayan pasado. En ese momento estaremos seguros de que nuestra unidad funciona para los Casos de Uso que hemos probado.
- Opcionalmente podemos añadir nuevas pruebas para verificar más a fondo la unidad: añadir nuevos casos de uso más improbables (aunque posibles), pasar valores extremos a los métodos (negativos cuando se esperan positivos, cadenas vacías, punteros nulos, etc.), comprobando que la situación se controla y se retorna el error correspondiente, etc.
Si nunca habéis aplicado esta metodología, quizá estos pasos os resulten demasiado extraño o no quede muy claro cómo habría que aplicarlo a un caso real. Pongamos un ejemplo. Supongamos que nos han encargado realizar una unidad que se encargue de enviar correos electrónicos. Esa unidad irá integrada en un sistema más grande, concretamente en la parte que se encarga de enviar informes de error o sugerencias de los usuarios que están usando el programa. Aplicando la metodología TDD seguiremos los siguientes pasos:
- Pensar en la funcionalidad que queremos desarrollar: si lo que tenemos que desarrollar es un algo que envíe correos electrónicos, uno de los Casos de Uso será enviar un correo electrónico. Además nos han dicho que se tiene que permitir el envío de hasta un archivo adjunto, así que otro caso de uso será enviar un correo con un archivo adjunto.
- Escribir el pseudocódigo de uno o varios casos de uso.
Parar enviar un correo electrónico:
Para enviar un correo electrónico es necesario conocer
un servidor SMTP.
Con esta función establecemos los datos del servidor
SMTP a través del que enviaremos el correo
EstablecerServidorEnvio(ip, puerto)
Con esta llamada se enviará el correo
EnviarCorreo(dirección origen, dirección destino, asunto, cuerpo)
Para enviar un correo electrónico con adjuntos:
Establecemos el servidor de envío
EstablecerServidorEnvio(ip, puerto)
Cargamos el adjunto y los metemos en un buffer
adjunto = CargarAdjunto("C:\ruta\nombre.ext");
Tenemos que añadir un nuevo parámetro para pasar el adjunto
Si no queremos enviar adjuntos pasaremos NULL
EnviarCorreo(origen, destino, asunto, cuerpo, adjunto);
Como veis, nos han salido varias funciones/métodos que tenemos que implementar, obteniendo así una lista de tareas. Entre las tareas a completar está la implementación de las funciones: EstablecerServidorEnvio, CargarAdjunto y EnviarCorreo.
- Codificar ese pseudocódigo de cada uno de los ejemplos en forma de Caso de Prueba:---Parar enviar un correo electrónico:
int ProbarEnviarCorreo()
{
char *ip;
int puerto;
char *origen, *destino, *asunto, *cuerpo;
datos del servidor
ip = "127.0.0.1";
puerto = 23;
si la función retorna error, no pasamos la prueba
if ( !EstablecerServidorEnvio(ip, puerto) )
return (FALSE);
datos del correo
origen = "yo@mismo.com";
destino = "tu@mismo.com";
asunto = "un correo de prueba";
cuerpo = "texto del correo de prueba\r\nChao pescao.";
el último parámetro indica que no se envía adjunto
if ( !EnviarCorreo(origen, destino, asunto, cuerpo, NULL) )
return (FALSE);
return (TRUE);
}
Para enviar un correo electrónico con adjuntos:
int ProbarEnviarCorreoConAdjunto()
{
char *ip;
int puerto;
char *origen, *destino, *asunto, *cuerpo;
char *ruta;
void *adjunto;
datos del servidor
ip = "127.0.0.1";
puerto = 23;
if ( !EstablecerServidorEnvio(ip, puerto) )
return (FALSE);
datos del adjunto
ruta = "C:\\fichero.ext";
adjunto = CargarAdjunto(ruta);
if (adjunto NULL)
return (FALSE);
datos del correo
origen = "yo@mismo.com";
destino = "tu@mismo.com";
asunto = "un correo de prueba";
cuerpo = "texto del correo con adjunto\r\nChao pescao.";
if ( !EnviarCorreo(origen, destino, asunto, cuerpo, adjunto) )
return (FALSE);
return (TRUE);
}
- Compilar ese Caso de Prueba. La compilación falla porque no encuentra las funciones EstablecerServidorEnvio, CargarAdjunto o EnviarCorreo.
- Declararemos las funciones, dejando el cuerpo vacío y que retorne error en todas ellas.
- Compilamos de nuevo las pruebas: ahora funcionan porque ya se encuentran las funciones.
- Ejecutamos las pruebas: fallarán todas porque lo único que hacen las funciones es retornar error.
- Codificaremos las tres funciones. Durante la codificación nos damos cuenta de algún detalle que no hemos tenido en cuenta: la función CargarAdjunto debe retornar el número de bytes que ocupa el archivo adjunto, además ese número de bytes hay que pasarlo también a la función EnviarCorreo. También necesitamos darle un nombre al fichero adjunto. Esto supone cambios en el prototipo de las funciones, así que tendremos que modificar también las pruebas. Finalmente, decidimos definir una estructura que represente el fichero adjunto, y pasaremos esta estructura a la función EnviarCorreo.
Finalmente, las prueba de envío de correo con adjunto quedará así: int ProbarEnviarCorreoConAdjunto()
{
char *ip;
int puerto;
char *origen, *destino, *asunto, *cuerpo;
char *ruta;
ADJUNTO adjunto;
datos del servidor
ip = "127.0.0.1";
puerto = 23;
if ( !EstablecerServidorEnvio(ip, puerto) )
return (FALSE);
datos del adjunto. Ahora se usa un struct
adjunto.ruta = "C:\\fichero.ext";
adjunto.datos = NULL;
se rellenará CargarAdjunto
adjunto.size = ; se rellenará CargarAdjunto
if ( !CargarAdjunto(&adjunto) )
return (FALSE);
datos del correo
origen = "yo@mismo.com";
destino = "tu@mismo.com";
asunto = "un correo de prueba";
cuerpo = "texto del correo con adjunto\r\nChao pescao.";
ahora se pasa la estructura del adjunto
if ( !EnviarCorreo(origen, destino, asunto, cuerpo, &adjunto) )
return (FALSE);
return (TRUE);
}
- Después de estas pequeñas correcciones, ejecutaremos las pruebas las veces que sean necesarias hasta que pasen correctamente, momento en el que daremos por terminado el ciclo.
- Para asegurarnos bien, verificaremos cómo se comportan las funciones antes parámetros incorrectos:
int ProbarEnviarCorreo()
{
char *ip
int puerto;
char *origen, *destino, *asunto, *cuerpo;
datos del servidor
ip = "127.0.0.1";
puerto = 23;
datos del correo
origen = "yo@mismo.com";
destino = "tu@mismo.com";
asunto = "un correo de prueba";
cuerpo = "texto del correo de prueba\r\nChao pescao.";
llamar a EnviarCorreo antes de establecer el servidor
Debe retornar error, así que si retorna ok, no se pasa la prueba
if ( EnviarCorreo(NULL, destino, asunto, cuerpo, NULL) )
return (FALSE);
dirección ip incorrecta.
Si retorna Ok, no pasa la prueba.
if ( EstablecerServidorEnvio(NULL, puerto) )
return (FALSE);
if ( EstablecerServidorEnvio(, puerto) )
return (FALSE);
// puerto incorrecto
if ( EstablecerServidorEnvio(ip, -1) )
return (FALSE);
if ( EstablecerServidorEnvio(ip, ) )
return (FALSE);
// caso correcto
if ( !EstablecerServidorEnvio(ip, puerto) )
return (FALSE);
// datos del correo
origen = "yo@mismo.com";
destino = "tu@mismo.com";
asunto = "un correo de prueba";
cuerpo = "texto del correo de prueba\r\nChao pescao.";
// dirección origen incorrecta
if ( EnviarCorreo(NULL, destino, asunto, cuerpo, NULL) )
return (FALSE);
if ( EnviarCorreo("yo", destino, asunto, cuerpo, NULL) )
return (FALSE);
if ( EnviarCorreo(, destino, asunto, cuerpo, NULL) )
return (FALSE);
dirección destino incorrecta
if ( EnviarCorreo(origen, NULL, asunto, cuerpo, NULL) )
return (FALSE);
if ( EnviarCorreo(origen, "yo", asunto, cuerpo, NULL) )
return (FALSE);
if ( EnviarCorreo(origen, "", asunto, cuerpo, NULL) )
return (FALSE);
asunto incorrecto
if ( EnviarCorreo(origen, destino, NULL, cuerpo, NULL) )
return (FALSE);
cuerpo incorrecto
if ( EnviarCorreo(origen, destino, asunto, NULL, NULL) )
return (FALSE);
caso correcto
if ( !EnviarCorreo(origen, destino, asunto, cuerpo, NULL) )
return (FALSE);
return (TRUE);
}
Como habéis visto, durante las propia etapa de pruebas nos hemos dado cuenta de ciertos errores de diseño. Si no hubiéramos aplicado la metodología TDD, habríamos codificado la unidad para enviar correos la primera vez, y después nos habríamos dado cuenta de que no es correcta y tendríamos que haberla codificado por segunda vez. Escribiendo las pruebas antes, hemos analizado y diseñado más detalladamente la unidad, así que en el momento de codificarla tenemos una idea mucho más clara y precisa de lo que debe hacer y cómo lo debe hacer.