0% encontró este documento útil (0 votos)
92 vistas

Java

El documento describe técnicas de desarrollo de software como la refactorización, pruebas de unidad y TDD. Incluye un ejemplo de generación de números primos que es refactorizado en varias etapas para mejorar su diseño y legibilidad.

Cargado por

Marco
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
92 vistas

Java

El documento describe técnicas de desarrollo de software como la refactorización, pruebas de unidad y TDD. Incluye un ejemplo de generación de números primos que es refactorizado en varias etapas para mejorar su diseño y legibilidad.

Cargado por

Marco
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 35

Técnicas útiles

en el desarrollo de software
Refactorización
Ejemplo: Generación de números primos
¿Cuándo hay que refactorizar?
Algunas refactorizaciones comunes
Pruebas de unidad
JUNIT
Ejemplo: La clase Money
TDD [Test-Driven Development]
Caso práctico: Bolera

Bibliografía
Robert C. Martin:
“Agile Software Development: Principles, Patterns, and Practices”.
Prentice Hall, 2003. ISBN 0-13-597444-5.
Martin Fowler:
“Refactoring: Improving the design of existing code”.
Addison-Wesley, 2000. ISBN 0-201-48567-2.
Kent Beck:
“Test-Driven Development by Example”.
Addison-Wesley, 2003. ISBN 0-321-14653-0
Refactorización
Definición
Refactorización (n)
Cambio realizado a la estructura interna del software
para hacerlo… más fácil de comprender
y más fácil de modificar
sin cambiar su comportamiento observable.
Refactorizar (v)
Reestructurar el software
aplicando una secuencia de refactorizaciones.

¿Por qué se “refactoriza” el software?


1. Para mejorar su diseño
a. Conforme se modifica, el software pierde su estructura.
b. Eliminar código duplicado simplificar su mantenimiento.

2. Para hacerlo más fácil de entender


p.ej. La legibilidad del código facilita su mantenimiento

3. Para encontrar errores


p.ej. Al reorganizar un programa, se pueden apreciar con mayor
facilidad las suposiciones que hayamos podido hacer.

4. Para programar más rápido

Al mejorar el diseño del código, mejorar su legibilidad


y reducir los errores que se cometen al programar,
se mejora la productividad de los programadores.
Técnicas útiles en el desarrollo de software -1- © Fernando Berzal
Ejemplo
Generación de números primos
© Robert C. Martin, “The Craftsman” column,
Software Development magazine, julio-septiembre 2002

Para conseguir un trabajo nos plantean el siguiente problema…


Implementar una clase que sirva para calcular todos los números
primos de 1 a N utilizando la criba de Eratóstenes

Una primera solución


Necesitamos crear un método que reciba como parámetro un valor
máximo y devuelva como resultado un vector con los números primos.

Para intentar lucirnos, escribimos…

/**
* Clase para generar todos los números primos de 1 hasta
* un número máximo especificado por el usuario. Como
* algoritmo se utiliza la criba de Eratóstenes.
* <p>
* Eratóstenes de Cirene (276 a.C., Cirene, Libia – 194
* a.C., Alejandría, Egipto) fue el primer hombre que
* calculó la circunferencia de la Tierra. También
* se le conoce por su trabajo con calendarios que ya
* incluían años bisiestos y por dirigir la mítica
* biblioteca de Alejandría.
* <p>
* El algoritmo es bastante simple: Dado un vector de
* enteros empezando en 2, se tachan todos los múltiplos
* de 2. A continuación, se encuentra el siguiente
* entero no tachado y se tachan todos sus múltiplos. El
* proceso se repite hasta que se pasa de la raíz cuadrada
* del valor máximo. Todos los números que queden sin
* tachar son números primos.
*
* @author Fernando Berzal
* @version 1.0 Enero’2005 (FB)
*/

Técnicas útiles en el desarrollo de software -2- © Fernando Berzal


El código lo escribimos todo en un único (y extenso) método, que
procuramos comentar correctamente:

public class Criba


{
/**
* Generar números primos de 1 a max
* @param max es el valor máximo
* @return Vector de números primos
*/
public static int[] generarPrimos (int max)
{
int i,j;

if (max >= 2) {

// Declaraciones
int dim = max + 1; // Tamaño del array
boolean[] esPrimo = new boolean[dim];

// Inicializar el array
for (i=0; i<dim; i++)
esPrimo[i] = true;

// Eliminar el 0 y el 1, que no son primos


esPrimo[0] = esPrimo[1] = false;

// Criba
for (i=2; i<Math.sqrt(dim)+1; i++) {
if (esPrimo[i]) {
// Eliminar los múltiplos de i
for (j=2*i; j<dim; j+=i)
esPrimo[j] = false;
}
}

// ¿Cuántos primos hay?


int cuenta = 0;

for (i=0; i<dim; i++) {


if (esPrimo[i])
cuenta++;
}

Técnicas útiles en el desarrollo de software -3- © Fernando Berzal


// Rellenar el vector de números primos
int[] primos = new int[cuenta];

for (i=0, j=0; i<dim; i++) {


if (esPrimo[i])
primos[j++] = i;
}

return primos;

} else { // max < 2

return new int[0]; // Vector vacío


}
}
}

Para comprobar que nuestro código funciona bien, decidimos probarlo


con un programa que incluye distintos casos de prueba (para lo que
usaremos la herramienta JUnit, disponible en http://www.junit.org/):

import junit.framework.*; // JUnit


import java.util.*;

// Clase con casos de prueba para Criba

public class CribaTest extends TestCase


{
// Programa principal (usa un componente de JUnit)

public static void main(String args[])


{
junit.swingui.TestRunner.main (
new String[] {"CribaTest"});
}

// Constructor

public CribaTest (String nombre)


{
super(nombre);
}
Técnicas útiles en el desarrollo de software -4- © Fernando Berzal
// Casos de prueba

public void testPrimos()


{
int[] nullArray = Criba.generarPrimos(0);
assertEquals(nullArray.length, 0);

int[] minArray = Criba.generarPrimos(2);


assertEquals(minArray.length, 1);
assertEquals(minArray[0], 2);

int[] threeArray = Criba.generarPrimos(3);


assertEquals(threeArray.length, 2);
assertEquals(threeArray[0], 2);
assertEquals(threeArray[1], 3);

int[] centArray = Criba.generarPrimos(100);


assertEquals(centArray.length, 25);
assertEquals(centArray[24], 97);
}
}

Al ejecutar los casos de prueba, conseguimos tener ciertas garantías de


que el programa funciona correctamente:

Técnicas útiles en el desarrollo de software -5- © Fernando Berzal


Después de eso, vamos orgullosos a enseñar nuestro programa y…
un programador no dice que, si queremos el trabajo, mejor no le
enseñemos eso al jefe de proyecto (que no tiene demasiada paciencia)

Veamos qué cosas hemos de mejorar…


Primeras mejoras
Parece evidente que nuestro método generarPrimos realiza tres
funciones diferentes, por lo que de generarPrimos extraemos tres
métodos diferentes. Además, buscamos un nombre más adecuado para
la clase y eliminamos todos los comentarios innecesarios.
/**
* Esta clase genera todos los números primos de 1 hasta un
* número máximo especificado por el usuario utilizando la
* criba de Eratóstenes
* <p>
* Dado un vector de enteros empezando en 2, se tachan todos
* los múltiplos de 2. A continuación, se encuentra el
* siguiente entero no tachado y se tachan sus múltiplos.
* Cuando se llega a la raíz cuadrada del valor máximo, los
* números que queden sin tachar son los números primos
*
* @author Fernando Berzal
* @version 2.0 Enero'2005 (FB)
*/

public class GeneradorDePrimos


{
private static int dim;
private static boolean esPrimo[];
private static int primos[];

public static int[] generarPrimos (int max)


{
if (max < 2) {
return new int[0]; // Vector vacío
} else {
inicializarCriba(max);
cribar();
rellenarPrimos();
return primos;
}
}
Técnicas útiles en el desarrollo de software -6- © Fernando Berzal
private static void inicializarCriba (int max)
{
int i;
dim = max + 1;
esPrimo = new boolean[dim];
for (i=0; i<dim; i++)
esPrimo[i] = true;
esPrimo[0] = esPrimo[1] = false;
}
private static void cribar ()
{
int i,j;
for (i=2; i<Math.sqrt(dim)+1; i++) {
if (esPrimo[i]) {
// Eliminar los múltiplos de i
for (j=2*i; j<dim; j+=i)
esPrimo[j] = false;
}
}
}
private static void rellenarPrimos ()
{
int i, j, cuenta;
// Contar primos
cuenta = 0;
for (i=0; i<dim; i++)
if (esPrimo[i])
cuenta++;
// Rellenar el vector de números primos
primos = new int[cuenta];
for (i=0, j=0; i<dim; i++)
if (esPrimo[i])
primos[j++] = i;
}
}

Los mismos casos de prueba de antes nos permiten comprobar que,


tras la refactorización, el programa sigue funcionando correctamente.
Técnicas útiles en el desarrollo de software -7- © Fernando Berzal
Un segundo intento
El código ha mejorado pero aún es algo más enrevesado de la cuenta:
eliminamos la variable dim (nos vale esPrimo.length),
elegimos identificadores más adecuados para los métodos y
reorganizamos el interior del método inicializarCandidatos
(el antiguo inicializarCriba).

public class GeneradorDePrimos


{
private static boolean esPrimo[];
private static int primos[];

public static int[] generarPrimos (int max)


{
if (max < 2) {
return new int[0];
} else {
inicializarCandidatos(max);
eliminarMultiplos();
obtenerCandidatosNoEliminados();
return primos;
}
}

private static void inicializarCandidatos (int max)


{
int i;
esPrimo = new boolean[max+1];
esPrimo[0] = esPrimo[1] = false;
for (i=2; i<esPrimo.length; i++)
esPrimo[i] = true;
}

private static void eliminarMultiplos ()


… // Código del antiguo método cribar()
private static void obtenerCandidatosNoEliminados ()
… // Código del antiguo método rellenarPrimos()
}

El código resulta más fácil de leer tras la refactorización.


Técnicas útiles en el desarrollo de software -8- © Fernando Berzal
Mejoras adicionales
El bucle anidado de eliminarMultiplos podía eliminarse si usamos
un método auxiliar para eliminar los múltiplos de un número concreto.
Por otro lado, la raíz cuadrada que aparece en eliminarMultiplos
no queda muy claro de dónde proviene (en realidad, es el valor
máximo que puede tener el menor factor de un número no primo
menor o igual que N). Además, el +1 resulta innecesario.

private static void eliminarMultiplos ()


{
int i;
for (i=2; i<maxFactor(); i++)
if (esPrimo[i])
eliminarMultiplosDe(i);
}

private static int maxFactor ()


{
return (int) Math.sqrt(esPrimo.length) + 1;
}

private static void eliminarMultiplosDe (int i)


{
int multiplo;
for ( multiplo=2*i;
multiplo<esPrimo.length;
multiplo+=i)
esPrimo[multiplo] = false;
}

De forma análoga, el método obtenerCandidatosNoEliminados


tiene dos partes bien definidas, por lo que podemos extraer un método
que se limite a contar el número de primos obtenidos…

Hemos ido realizando cambios que mejoran la implementación


sin modificar su comportamiento externo (su interfaz),
algo que verificamos tras cada refactorización
volviendo a ejecutar los casos de prueba.
Técnicas útiles en el desarrollo de software -9- © Fernando Berzal
¿Cuándo hay que refactorizar?

Cuando se está escribiendo nuevo código


Al añadir nueva funcionalidad a un programa (o modificar su
funcionalidad existente), puede resultar conveniente refactorizar:

- para que éste resulte más fácil de entender, o

- para simplificar la implementación de las nuevas funciones


cuando el diseño no estaba inicialmente pensado para lo que
ahora tenemos que hacer.

Cuando se intenta corregir un error


La mayor dificultad de la depuración de programas radica en que
hemos de entender exactamente cómo funciona el programa para
encontrar el error. Cualquier refactorización que mejore la calidad del
código tendrá efectos positivos en la búsqueda del error.

De hecho, si el error “se coló” en el código es porque


no era lo suficientemente claro cuando lo escribimos.

Cuando se revisa el código


Una de las actividades más productivas desde el punto de vista de la
calidad del software es la realización de revisiones del código
(recorridos e inspecciones). Llevada a su extremo, la programación
siempre se realiza por parejas (pair programming, una de las técnicas
de la programación extrema, XP [eXtreme Programming]).

¿Por qué es importante la refactorización?


Cuando se corrige un error o se añade una nueva función, el valor
actual de un programa aumenta. Sin embargo, para que un programa
siga teniendo valor, debe ajustarse a nuevas necesidades (mantenerse),
que puede que no sepamos prever con antelación. La refactorización,
precisamente, facilita la adaptación del código a nuevas necesidades.
Técnicas útiles en el desarrollo de software - 10 - © Fernando Berzal
¿Qué síntomas indican que debería refactorizar?

El código es más difícil de entender (y, por tanto, de cambiar) cuando:

- usa identificadores mal escogidos,

- incluye fragmentos de código duplicados,

- incluye lógica condicional compleja,

- los métodos usan un número elevado de parámetros,

- incluye fragmentos de código secuencial muy extensos,

- está dividido en módulos enormes (estructura monolítica),

- los módulos en los que se divide no resultan razonables desde el


punto de vista lógico (cohesión baja),

- los distintos módulos de un sistema están relacionados con otros


muchos módulos de un sistema (acoplamiento fuerte),

- un método accede continuamente a los datos de un objeto de una


clase diferente a la clase en la que está definida (posiblemente, el
método debería pertenecer a la otra clase),

- una clase incluye variables de instancia que deberían ser


variables locales de alguno(s) de sus métodos,

- métodos que realizan funciones análogas se usan de forma


diferente (tienen nombres distintos y/o reciben los parámetros en
distinto orden),

- un comentario se hace imprescindible para poder entender un


fragmento de código (deberíamos re-escribir el código de forma
que podamos entender su significado).
NOTA: Esto no quiere decir que dejemos de comentar el código.
Técnicas útiles en el desarrollo de software - 11 - © Fernando Berzal
Algunas refactorizaciones comunes
Algunos IDEs (p.ej. Eclipse) permiten realizarlas de forma automática.

Renombrar método [rename method]


Cuando el nombre de un método no refleja su propósito

1. Declarar un método nuevo con el nuevo nombre.

2. Copiar el cuerpo del antiguo método al nuevo método


(y realizar cualquier modificación que resulte necesaria).

3. Compilar
(para verificar que no hemos introducido errores sintácticos)

4. Reemplazar el cuerpo del antiguo método por una llamada al


nuevo método (este paso se puede omitir si no se hace referencia
al antiguo método desde muchos lugares diferentes).

5. Compilar y probar.

6. Encontrar todas las referencias al antiguo método y cambiarlas


por invocaciones al nuevo método.

7. Eliminar el antiguo método.

8. Compilar y probar.

NOTA:
Si el antiguo método era un método público usado por otros
componentes o aplicaciones y no podemos eliminarlo, el antiguo
método se deja en su lugar (como una llamada al nuevo método)
y se marca como “deprecated” con Javadoc (@deprecated).

Técnicas útiles en el desarrollo de software - 12 - © Fernando Berzal


Extraer método [extract method]
Convertir un fragmento de código en un método
cuyo identificador explique el propósito del fragmento de código.

1. Crear un nuevo método y buscarle un identificador adecuado.

2. Copiar el fragmento de código en el cuerpo del método.

3. Buscar en el código extraído referencias a variables locales del


método original (estas variables se convertirán en los
parámetros, variables locales y resultado del nuevo método):

a. Si una variable se usa sólo en el fragmento de código


extraído, se declara en el nuevo método como variable
local de éste.

b. Si el valor de una variable sólo se lee en el fragmento de


código extraído, la variable será un parámetro del nuevo
método.

c. Si una variable se modifica en el fragmento de código


extraído, se intenta convertir el nuevo método en una
función que da como resultado el valor que hay que
asignarle a la variable modificada.

d. Compilar el código para comprobar que todas las


referencias a variables son válidas

4. Reemplazar el fragmento de código en el método original por


una llamada al nuevo método.

5. Eliminar las declaraciones del método original correspondientes


a las variables que ahora son variables locales del nuevo método.

6. Compilar y probar.

Técnicas útiles en el desarrollo de software - 13 - © Fernando Berzal


Versión final
Como sabemos que no es muy recomendable usar demasiado a
menudo la palabra reservada static, con unos pequeños cambios
convertimos GeneradorDePrimos en una clase de la que se puedan
crear distintos objetos.

En primer lugar, modificamos los casos de prueba con los que


comprobaremos el funcionamiento de nuestro generador de primos:

import junit.framework.*;

public class GeneradorDePrimosTest extends TestCase


{
GeneradorDePrimos generador;
int[] primos;

public static void main(String args[])


{
junit.swingui.TestRunner.main(
new String[] {"GeneradorDePrimosTest"});
}

public GeneradorDePrimosTest(String name)


{
super(name);
}

public void testPrimos0()


{
generador = new GeneradorDePrimos(0);
primos = generador.getPrimos();
assertEquals( primos.length, 0);
}

public void testPrimos2()


{
generador = new GeneradorDePrimos(2);
primos = generador.getPrimos();
assertEquals(primos.length, 1);
assertEquals(primos[0], 2);
}
Técnicas útiles en el desarrollo de software - 10 - © Fernando Berzal
public void testPrimos3()
{
generador = new GeneradorDePrimos(3);
primos = generador.getPrimos();
assertEquals(primos.length, 2);
assertEquals(primos[0], 2);
assertEquals(primos[1], 3);
}

public void testPrimos100()


{
generador = new GeneradorDePrimos(100);
primos = generador.getPrimos();
assertEquals(primos.length, 25);
assertEquals(primos[24], 97);
}
}

A continuación, creamos un constructor para GeneradorDePrimos


(que construye el vector de números primos) y añadimos un método
getPrimos() con el cual acceder a este vector desde el exterior…

/**
* Esta clase genera todos los números primos de 1
* hasta un número máximo especificado por el usuario
* utilizando la criba de Eratóstenes.
* <p>
* Dado un vector de enteros empezando en 2, se tachan
* todos los múltiplos de 2. A continuación, se
* encuentra el siguiente entero no tachado y se
* tachan sus múltiplos. Los números que queden sin
* tachar al final son los números primos entre 1 y N.
*
* @author Fernando Berzal
* @version 3.0 Enero'2005 (FB)
*/

public class GeneradorDePrimos


{
private boolean esPrimo[];
private int primos[];

Técnicas útiles en el desarrollo de software - 11 - © Fernando Berzal


public GeneradorDePrimos (int max)
{
if (max < 2) {

primos = new int[0]; // Vector vacío

} else {

inicializarCandidatos(max);
eliminarMultiplos();
obtenerCandidatosNoEliminados();
}
}

public int[] getPrimos ()


{
return primos;
}

private void inicializarCandidatos (int max)


{
int i;

esPrimo = new boolean[max+1];

esPrimo[0] = esPrimo[1] = false;

for (i=2; i<esPrimo.length; i++)


esPrimo[i] = true;
}

private void eliminarMultiplos ()


{
int i;

for (i=2; i<maxFactorPrimo(); i++)


if (esPrimo[i])
eliminarMultiplosDe(i);
}

Técnicas útiles en el desarrollo de software - 12 - © Fernando Berzal


private int maxFactorPrimo ()
{
return (int) Math.sqrt(esPrimo.length) + 1;
}

private void eliminarMultiplosDe (int i)


{
int multiplo;

for ( multiplo=2*i;
multiplo<esPrimo.length;
multiplo+=i )
esPrimo[multiplo] = false;
}

private void obtenerCandidatosNoEliminados ()


{
int i, j;

primos = new int[numPrimos()];

for (i=0, j=0; i<esPrimo.length; i++) {


if (esPrimo[i])
primos[j++] = i;
}
}

private int numPrimos ()


{
int i;
int cuenta = 0;

for (i=0; i<esPrimo.length; i++) {


if (esPrimo[i])
cuenta++;
}

return cuenta;
}
}

Técnicas útiles en el desarrollo de software - 13 - © Fernando Berzal


Pruebas de unidad
con JUnit
Cuando se implementa software, resulta recomendable comprobar
que el código que hemos escrito funciona correctamente.

Para ello, implementamos pruebas que verifican


que nuestro programa genera los resultados que de él esperamos.

Conforme vamos añadiéndole nueva funcionalidad a un programa,


creamos nuevas pruebas con las que podemos medir nuestro progreso
y comprobar que lo que antes funcionaba sigue funcionando tras haber
realizado cambios en el código (test de regresión).

Las pruebas también son de vital importancia cuando refactorizamos


(aunque no añadamos nueva funcionalidad, estamos modificando la
estructura interna de nuestro programa y debemos comprobar que no
introducimos errores al refactorizar).

Automatización de las pruebas

Para agilizar la realización de las pruebas resulta práctico que un test


sea completamente automático y compruebe los resultados esperados.

û No es muy apropiado llamar a una función, guardar el resultado


en algún sitio y después tener que comprobar manualmente si el
resultado era el deseado.

ü Mantener automatizado un conjunto amplio de tests permite


reducir el tiempo que se tarda en depurar errores y en verificar la
corrección del código.
Técnicas útiles en el desarrollo de software - 14 - © Fernando Berzal
JUnit
Herramienta especialmente diseñada para implementar y automatizar
la realización de pruebas de unidad en Java.

Dada una clase de nuestra aplicación…

En una clase aparte definimos un conjunto de casos de prueba


- La clase hereda de junit.framework.TestCase
- Cada caso de prueba se implementa en un método aparte.
- El nombre de los casos de prueba siempre comienza por test.

import junit.framework.*;

public class CuentaTest extends TestCase


{

}

Cada caso de prueba invoca a una serie de métodos de nuestra clase


y comprueba los resultados que se obtienen tras invocarlos.
- Creamos uno o varios objetos de nuestra clase con new
- Realizamos operaciones con ellos.
- Definimos aserciones (condiciones que han de cumplirse).

public void testCuentaNueva ()


{
Cuenta cuenta = new Cuenta();
assertEquals(cuenta.getSaldo(), 0.00);
}

public void testIngreso ()


{
Cuenta cuenta = new Cuenta();
cuenta.ingresar(100.00);
assertEquals(cuenta.getSaldo(), 100.00);
}

Técnicas útiles en el desarrollo de software - 15 - © Fernando Berzal


Finalmente, ejecutamos los casos de prueba con JUnit:
§ Si todos los casos de prueba funcionan correctamente…

§ Si algún caso de prueba falla…

Tendremos que localizar el error y corregirlo


con ayuda de los mensajes que nos muestra JUnit.

MUY IMPORTANTE: Que nuestra implementación supere todos los


casos de prueba no quiere decir que sea correcta; sólo quiere decir que
funciona correctamente para los casos de prueba que hemos diseñado.

Técnicas útiles en el desarrollo de software - 16 - © Fernando Berzal


Apéndice: Cómo ejecutar JUnit desde nuestro propio código

Para lanzar JUnit desde nuestro propio código,


sin tener que ejecutar la herramienta a mano,
hemos de implementar el método main en nuestra clase
y definir un sencillo constructor.

import junit.framework.*;

public class CuentaTest extends TestCase


{
public static void main(String args[])
{
junit.swingui.TestRunner.main (
new String[] {"CuentaTest"});
}

public CuentaTest(String name)


{
super(name);
}

// Casos de prueba

}

Técnicas útiles en el desarrollo de software - 17 - © Fernando Berzal


Ejemplo: La clase Money
Basado en “Test-Driven Development by Example”, pp. 1-87 © Kent Beck

Vamos a definir una clase, denominada Money, para representar


cantidades de dinero que pueden estar expresadas en distintas monedas

Por ejemplo, queremos usar esta clase para generar informes como…

Empresa Acciones Precio Total


Telefónica 200 10 EUR 2000 EUR
Vodafone 100 50 EUR 5000 EUR
7000 EUR

El problema es que también nos podemos encontrar con situaciones


como la siguiente …

Empresa Acciones Precio Total


Microsoft 200 13 USD 2600 USD
Indra 100 50 EUR 5000 EUR
7000 EUR

donde hemos tenido que utilizar el tipo de cambio actual (1€=$1.30)

Comenzamos creando una clase para representar cantidades de dinero:

public class Money


{
private int cantidad;
private String moneda;
public Money (int cantidad, String moneda)
{
this.cantidad = cantidad;
this.moneda = moneda;
}
public int getCantidad() { return cantidad; }
public String getMoneda() { return moneda; }
}

Técnicas útiles en el desarrollo de software - 18 - © Fernando Berzal


Una de las cosas que tendremos que hacer es sumar cantidades, por lo
que podemos idear un caso de prueba como el siguiente:

import junit.framework.*;

public class MoneyTest extends TestCase


{
public void testSumaSimple()
{
Money m10 = new Money (10, "EUR");
Money m20 = new Money (20, "EUR");
Money esperado = new Money (30, "EUR");
Money resultado = m10.add(m20);
Assert.assertEquals(resultado,esperado);
}
}

Al idear el caso de prueba, nos estamos fijando en cómo tendremos


que usar nuestra clase en la práctica, lo que nos es extremadamente
útil para definir su interfaz.

En este caso, nos hace falta añadir un método add a la clase Money
para poder compilar y ejecutar el caso de prueba…

Creamos una implementación inicial de este método:

public Money add (Money m)


{
int total = getCantidad() + m.getCantidad();
return new Money( total, getMoneda());
}

Compilamos y ejecutamos el test para llevarnos una sorpresa…

Técnicas útiles en el desarrollo de software - 19 - © Fernando Berzal


El caso de prueba falla porque la comparación de objetos en Java, por
defecto, se limita a comparar referencias (no compara el estado de los
objetos, que es lo que podríamos pensar [erróneamente]).

La comparación de objetos en Java se realiza con el método equals,


que puede recibir como parámetro un objeto cualquiera.

Por tanto, hemos de definir el método equals en la clase Money:

public boolean equals (Object obj)


{
Money aux;
boolean iguales;
if (obj instanceof Money) {
aux = (Money) obj;
iguales = aux.getMoneda().equals(getMoneda())
&& (aux.getCantidad() == getCantidad());
} else {
iguales = false;
}
return iguales;
}

Volvemos a ejecutar el caso de prueba…

… y ahora sí podemos seguir avanzando


Técnicas útiles en el desarrollo de software - 20 - © Fernando Berzal
De todas formas, para asegurarnos de que todo va bien, creamos un
caso de prueba específico que verifique el funcionamiento de equals

public void testEquals()


{
Money m10 = new Money (10, "EUR");
Money m20 = new Money (20, "EUR");

Assert.assertEquals(m10,m10);
Assert.assertEquals(m20,m20);
Assert.assertTrue(!m10.equals(m20));
Assert.assertTrue(!m20.equals(m10));
Assert.assertTrue(!m10.equals(null));
}

Al ejecutar nuestros dos casos de prueba con JUNIT


vemos que todo marcha como esperábamos.

Sin embargo, comenzamos a ver que existe código duplicado


(y ya sabemos que eso no es una buena señal),
por lo que utilizamos la posibilidad que nos ofrece JUNIT
de definir variables de instancia en la clase que hereda de TestCase
(variables que hemos de inicializar en el método setUp())

public class MoneyTest extends TestCase


{
Money m10;
Money m20;

public void setUp()


{
m10 = new Money (10, "EUR");
m20 = new Money (20, "EUR");
}

}

Técnicas útiles en el desarrollo de software - 21 - © Fernando Berzal


Aparte de sumar cantidades de dinero, también tenemos que ser
capaces de multiplicar una cantidad por un número entero
(p.ej. número de acciones por precio de cada acción)

Podemos añadir un nuevo caso de prueba que utilice esta función:

public void testMultiplicar ()


{
Assert.assertEquals ( m10.times(2), m20 );
Assert.assertEquals ( m10.times(2), m20 );
Assert.assertEquals ( m10.times(10), m20.times(5));
}

Obviamente, también tendremos que definir times en Money:

public Money times (int n)


{
return new Money ( n*getCantidad(), getMoneda() );
}

Compilamos y ejecutamos los casos de prueba con JUnit:

Poco a poco, vamos añadiéndole funcionalidad a nuestra clase:


Cada vez que hacemos cambios, volvemos a ejecutar todos
los casos de prueba para confirmar que no hemos estropeado nada.
Técnicas útiles en el desarrollo de software - 22 - © Fernando Berzal
Una vez que hemos comprobado que ya somos capaces de hacer
operaciones cuando todo se expresa en la misma moneda, tenemos que
comenzar a trabajar con distintas monedas.

Por ejemplo:

public void testSumaCompleja ()


{
Money euros = new Money(100,"EUR");
Money dollars = new Money(130,"USD");
Money resultado = euros.add (dollars);
Money banco = Bank.exchange(dollars,"EUR")
Money esperado = euros.add(banco);
Assert.assertEquals ( resultado, esperado );
}

Para hacer el cambio de moneda, suponemos que tenemos acceso a un


banco que se encarga de hacer la conversión. Hemos de crear una
clase auxiliar Bank que se va a encargar de consultar los tipos de
cambio y aplicar la conversión correspondiente:

public class Bank


{
public static Money exchange
(Money dinero, String moneda)
{
int cantidad = 0;
if (dinero.getMoneda().equals(moneda)) {
cantidad = dinero.getCantidad();
} else if ( dinero.getMoneda().equals("EUR")
&& moneda.equals("USD")) {
cantidad = (130*dinero.getCantidad())/100;
} else if ( dinero.getMoneda().equals("USD")
&& moneda.equals("EUR")) {
cantidad = (100*dinero.getCantidad())/130;
}
return new Money(cantidad,moneda);
}
}

Técnicas útiles en el desarrollo de software - 23 - © Fernando Berzal


Por ahora, nos hemos limitado a realizar una conversión fija para
probar el funcionamiento de nuestra clase Money (en una aplicación
real tendríamos que conectarnos realmente con el banco).

Obviamente, al ejecutar nuestros casos de prueba se produce un error:

Hemos de corregir la implementación interna del método add de la


clase Money para que tenga en cuenta el caso de que las cantidades
correspondan a monedas diferentes:

public Money add (Money dinero)


{
Money convertido;
int total;

if (getMoneda().equals(dinero.getMoneda()))
convertido = dinero;
else
convertido = Bank.exchange(dinero, getMoneda());

total = getCantidad() + convertido.getCantidad();

return new Money( total, getMoneda());


}

Técnicas útiles en el desarrollo de software - 24 - © Fernando Berzal


Volvemos a ejecutar los casos de prueba
y comprobamos que, ahora sí, las sumas se hacen correctamente:

Si todavía no las tuviésemos todas con nosotros,


podríamos seguir añadiendo casos de prueba para adquirir
más confianza en la implementación que acabamos de realizar.

Por ejemplo, el siguiente caso de prueba comprueba


el funcionamiento del banco al realizar conversiones de divisas:

public void testBank ()


{
Money euros = new Money(10,"EUR");
Money dollars = new Money(13,"USD");
Assert.assertEquals (
Bank.exchange(dollars,"EUR"), euros );
Assert.assertEquals (
Bank.exchange(dollars,"USD"), dollars );
Assert.assertEquals (
Bank.exchange(euros, "EUR"), euros );
Assert.assertEquals (
Bank.exchange(euros, "USD"), dollars );
}

Técnicas útiles en el desarrollo de software - 25 - © Fernando Berzal


Comentarios finales: El método toString

Para que resulte más fácil interpretar


los mensajes generados por JUNIT,
resulta recomendable definir el método toString()
en todas las clases que definamos. Por ejemplo:

public class Money


{

public String toString ()
{
return getCantidad()+" "+getMoneda();
}
}

RECORDATORIO: toString() es un método


que se emplea en Java para convertir un objeto cualquiera
en una cadena de caracteres.

Teniendo definido el método anterior,


en nuestras aplicaciones podríamos escribir directamente…


Money share = new Money(13,"USD");
Money investment = share.times(200);
Money euros = Bank.exchange(investment,”EUR”);

System.out.println(investment + "(" + euros + ")");


y obtener como resultado en pantalla:

2600 USD (2000 EUR)

Técnicas útiles en el desarrollo de software - 26 - © Fernando Berzal


TDD
[Test-Driven Development]

Consiste en implementar las pruebas de unidad


antes incluso de comenzar a escribir el código de un módulo.

Las pruebas de unidad consisten en


comprobaciones (manuales o automatizadas)
que se realizan para verificar que el código
correspondiente a un módulo concreto de un sistema software
funciona de acuerdo con los requisitos del sistema.

Tradicionalmente,
las pruebas se realizan a posteriori

Los casos de prueba se suelen escribir después de implementar el


módulo cuyo funcionamiento pretenden verificar.

Como mucho, se preparan en paralelo si el programador


y la persona que realiza las pruebas [tester] no son la misma persona.

En TDD,
las pruebas se preparan antes de comenzar a escribir el código.

Primero escribimos un caso de prueba


y sólo después implementamos el código necesario
para que el caso de prueba se pase con éxito

Técnicas útiles en el desarrollo de software - 27 - © Fernando Berzal


Aunque pueda parecer extraño, TDD ofrece algunas ventajas:

Al escribir primero los casos de prueba, definimos de manera


formal los requisitos que esperamos que cumpla nuestra aplicación.

Los casos de prueba sirven de documentación del sistema.

Al escribir una prueba de unidad, pensamos en la forma correcta de


utilizar un módulo que aún no existe.

Hacemos hincapié en el diseño de la interfaz de un módulo antes


de centrarnos en su implementación (algo siempre bueno:
“la interfaz debe determinar la implementación, y no al revés”).

La ejecución de los casos de prueba se realiza de forma


automatizada (por ejemplo, con ayuda de JUNIT)

Al ejecutar los casos de prueba detectamos si hemos introducido


algún error al tocar el código para realizar cualquier cambio en
nuestra aplicación (ya sea para añadirle nuevas funciones o para
reorganizar su estructura interna [refactorización]).

Los casos de prueba nos permiten perder el miedo a realizar


modificaciones en el código

Tras realizar pequeñas modificaciones sobre el código,


volveremos a ejecutar los casos de prueba para comprobar
inmediatamente si hemos cometido algún error o no.

Los casos de prueba definen claramente cuándo termina nuestro


trabajo (cuando se pasan con éxito todas los casos de prueba).

Técnicas útiles en el desarrollo de software - 28 - © Fernando Berzal


El proceso de construcción de software se convierte en un ciclo:

1. Añadir un nuevo caso de prueba


que recoja algo que nuestro módulo debe realizar correctamente.

2. Ejecutar los casos de prueba


para comprobar que el caso recién añadido falla.

3. Realizar pequeños cambios en la implementación


(en función de lo que queremos que haga nuestra aplicación).

4. Ejecutar los casos de prueba


hasta que todos se vuelven a pasar con éxito.

5. Refactorizar el código para mejorar su diseño (eliminar código


duplicado, extraer métodos, renombrar identificadores…)

6. Ejecutar los casos de prueba


para comprobar que todo sigue funcionando correctamente.

7. Volver al paso inicial


Técnicas útiles en el desarrollo de software - 29 - © Fernando Berzal
Caso práctico: Bolera

El problema consiste en…


obtener la puntuación de un jugador en una partida de bolos.

Una posible solución…


y el proceso seguido para obtenerla en

“The Bowling Game. An example of test-first pair programming”


© Robert C. Martin & Robert S. Koss, 2001
http://www.objectmentor.com/resources/articles/xpepisode.htm

Robert C. Martin:
“Agile Software Development: Principles, Patterns, and Practices”.
Prentice Hall, 2003. ISBN 0-13-597444-5.

Técnicas útiles en el desarrollo de software - 30 - © Fernando Berzal

También podría gustarte