Tema 3. Test Driven Development

10  24  Descargar (0)

Texto completo

(1)

1

Tema 3.

Test Driven Development

Ejercicios Resueltos

Ejercicio 01.

Desarrolle mediante TDD una implementación del algoritmo de la Criba de Eratóstenes

para calcular la lista de los números primos desde 2 hasta un número n indicado. Si no existiera

ningún primo, el algoritmo devolverá una lista vacía. El algoritmo de la criba de Eratóstenes se

muestra a continuación.

1.

Se crea una lista con los números desde 2 hasta n.

2.

Se elige el siguiente número x no marcado (inicialmente el 2).

3.

Se marcan todos los múltiplos de dicho número (x*2, x*3, etc.).

4.

Se repite desde el paso 2.

Cuando se ha terminado con todos los números aquellos que queden sin marcar son

primos. Más información sobre la criba de Eratóstenes en la Wikipedia:

http://en.wikipedia.org/wiki/Sieve_of_Eratosthenes

Solución

Antes de comenzar con la implementación es interesante pararse un momento a

estudiar los posibles casos de prueba de este algoritmo.

El único valor de entrada de las pruebas es el número n límite para calcular los

números primos. Los casos de prueba se pueden dividir en varias particiones equivalentes en

función de dicho n. Vamos a describir estas particiones a continuación.

La primera partición engloba a todos los números menores de 2. Para cualquier valor

de dicha partición el resultado de esta implementación será siempre el mismo: una lista vacía

de números.

Después, podemos definir tantas particiones como valores son necesarios para incluir

a un nuevo número primo. También podemos jugar con particiones que terminan justo en un

valor primo o justo después (por ejemplo, calcular todos los primos hasta 11 o hasta 12). Todos

son equivalentes a la hora de generar ya que el proceso para calcularlo son los mismos, pero

alguna prueba adicional puede ayudarnos a detectar errores ocultos.

Veamos las primeras evoluciones aplicando TDD.

@Test

public void testCalculaConValorInicialUno() {

List<Integer> l = CrivaDeEratosthenes.Calcula(1); assertTrue(l.isEmpty());

}

// Código

(2)

2

public static List<Integer> Calcula(int i) { return new ArrayList<Integer>(); }

}

//--- @Test

public void testCalculaConValorInicialDos() {

List<Integer> l = CrivaDeEratosthenes.Calcula(2); // (*)

assertEquals(1, l.size());

assertEquals(new Integer(2), l.get(0)); }

// Código

public static List<Integer> Calcula(int i) { List<Integer> l = new ArrayList<Integer>(); if (i >= 2)

l.add(2); return l;

}

(*) Aunque los dos asserts verifican lo mismo, con el primer assert evitamos que la

prueba falle por una excepción si no hay ningún elemento en la lista. Hacer este cambio hace

más legible la traza de la prueba cuando no hay ningún elemento en la lista. Al final de la traza

veremos una manera más cómoda de escribir este tipo de asserts utilizando la librería de Java.

Continuamos aplicando TDD.

@Test

public void testCalculaConValorInicialUno() {

List<Integer> l = CrivaDeEratosthenes.Calcula(1); assertTrue(l.isEmpty());

} @Test

public void testCalculaConValorInicialDos() {

List<Integer> l = CrivaDeEratosthenes.Calcula(2); assertEquals(new Integer(2), l.get(0));

} @Test

public void testCalculaConValorInicialTres() {

List<Integer> l = CrivaDeEratosthenes.Calcula(3); assertEquals(2, l.size());

assertEquals(new Integer(2), l.get(0)); assertEquals(new Integer(3), l.get(1)); }

// Código

public static List<Integer> Calcula(int i) { List<Integer> l = new ArrayList<Integer>(); if (i >= 2) { l.add(2); l.add(3); } return l; }

(3)

3

En este nuevo paso vemos dos detalles interesantes. La primera es que ha sido

necesario quitar el assert que pusimos para evitar un error por excepción. La segunda es que

introducir una nueva prueba no ha hecho avanzar. Es necesario cambiar de enfoque.

Llegados a este punto ya nos damos cuenta de que los casos de prueba no ayudan a

evolucionar el código. Tendríamos que dar un paso muy grande con muchos cambios que

pueden salir mal para implementar el código del algoritmo.

Este es el momento de buscar alternativas para hacer pruebas más pequeñas y avanzar

pasos más diminutos. Para ello cada paso del algoritmo será un método y cada uno de los

métodos irá creciendo guiado por pruebas.

Aunque dichos métodos deberían ser privados, los pondremos con el ámbito de

visibilidad necesario para poder probarlos. En el próximo módulo veremos las técnicas y

herramientas para poder probar métodos privados.

Empezamos con una primera prueba que nos haga avanzar en este paso. El primer

paso que vamos a abordar es crear una matriz de booleanos para indicar qué números están

marcados y cuáles no.

@Test

public void testCreaListaDeNumerosSinMarcar() { int tope = 4; List<Boolean> l = CrivaDeEratosthenes.CreaListaDeNumerosSinMarcar(tope); assertEquals((tope+1), l.size()); for (Boolean b:l) { assertFalse(b); } } // Código

public static List<Boolean> CreaListaDeNumerosSinMarcar(int i) { List<Boolean> lb = new ArrayList<Boolean>();

for (int c=0; c<=i; c++) lb.add(false); return lb;

}

Necesitamos incrementar el tope en 1 ya que para que el número 4 aparezca en la lista de

marcados, es necesario que la lista tenga 5 elementos (del 0 al 5). Como trabajamos con listas,

ignoraremos las posiciones 0 y 1 que siempre serán false ya que no intervienen. Continuamos.

@Test

public void testMarcarMultiplos() { int tope = 2; List<Boolean> l = CrivaDeEratosthenes.CreaListaDeNumerosSinMarcar(2); CrivaDeEratosthenes.MarcarMultiplos(l); assertFalse(l.get(2)); } // Código

public static void MarcarMultiplos(List<Boolean> l) { }

¡Cuidado! Hemos descubierto un mal caso de prueba, el nombre es poco descriptivo y

no le estamos pidiendo a nuestro sistema que haga nada por eso un método vacío lo pasa.

Vamos a cambiar este caso de prueba. Vamos a utilizar como valor de prueba 4 porque es el

primer valor que introduce un cambio. Continuamos.

(4)

4

@Test

public void testMarcarMultiplosHasta4() {

List<Boolean> l = CrivaDeEratosthenes.CreaListaDeNumerosSinMarcar(4); CrivaDeEratosthenes.MarcarMultiplos(l); assertFalse(l.get(2)); assertFalse(l.get(3)); assertTrue(l.get(4)); } // Código

public static void MarcarMultiplos(List<Boolean> l) { for (int num = 2; num < l.size(); num++) {

for (int mul = (num*2); mul < l.size(); mul += num) { l.set(mul, true); } } } //--- @Test

public void testCrearListaDePrimosHasta4() {

List<Boolean> l =

CrivaDeEratosthenes.CreaListaDeNumerosSinMarcar(4); CrivaDeEratosthenes.MarcarMultiplos(l);

List<Integer> primos = CrivaDeEratosthenes.CreaListaDePrimos(l); assertEquals(2, primos.size());

assertEquals(new Integer(2), primos.get(0)); assertEquals(new Integer(3), primos.get(1)); }

// Código

public static List<Integer> CreaListaDePrimos(List<Boolean> l) { List<Integer> lb = new ArrayList<Integer>();

for (int c = 2; c < l.size();c++) { if (!l.get(c)) { lb.add(c); } } return lb; }

Ya tenemos implementados y probados todos los pasos. Ahora es el momento de

refactorizar el método que calcula la criba de Eratóstenes y comprobar que las primeras

pruebas que escribimos siguen funcionando. Veamos la refactorización.

// Código

public static List<Integer> Calcula(int i) {

List<Boolean> lb = CreaListaDeNumerosSinMarcar(i); MarcarMultiplos(lb);

return CreaListaDePrimos(lb); }

Las pruebas siguen funcionando por lo que ya podemos dar por terminada la

implementación. Si embargo podemos añadir algunas pruebas más jugando con las particiones

que comentamos al principio. Por ejemplo:

(5)

5

@Test

public void testGeneraPrimosHastaDoce() {

List<Integer> l = CrivaDeEratosthenes.Calcula(12); Assert.assertEquals(l, Arrays.asList(2, 3, 5, 7, 11)); }

Consideraciones finales

Este desarrollo ha tenido una carencia. No se ha podido hacer TDD para definir que el

método principal llame a los demás métodos ni verifica si el orden en que los llama es el

correcto, con lo que hemos diseñado esa parte sin el soporte de pruebas.

Este tipo de TDD lo realizaremos mediante mocks los cuáles estudiaremos en el

siguiente módulo.

Ejercicio 02.

Se desea crear una clase que funcione como un contador. Se cuenta con los siguientes

requisitos.

Al crear el contador indicamos el valor inicial del mismo, el incremento y el valor

límite.

El valor inicial y el incremento tomarán un valor de 0 y 1 respectivamente si no se

indica nada. El límite es necesario indicarlo siempre.

Ninguno de los tres valores (valor inicial, incremento y límite) pueden cambiarse una

vez creado el contador

Al incrementar el contador se suma al valor actual el incremento y nos indican si se

superó el límite.

Cuando se supere el límite, el valor actual del contador vuelve a ser el valor inicial.

En cualquier momento se puede conocer el valor actual del contador y

E cualquier momento se puede establecer el contador a su valor inicial.

Implemente los requisitos anteriores utilizando TDD.

Solución

Esta solución muestra la línea temporal del trabajo hecho. Cada boque de código

(entre dos comentarios con guiones) es la implementación de una característica en el código.

Primero se muestra el código de prueba y, después, la implementación. También se indican las

refactorizaciones realizadas.

(6)

6

Esta misma traza y el código Java obtenido pueden descargarse en la sección de

materiales del curso. En el boletín de ejercicios de este tema se plantean cuestiones

adicionales a partir de esta solución.

@Test

public void testVerValorDelContadorPorDefecto() { ContadorCircular cc = new ContadorCircular(); assertEquals(0, cc.getValor());

} // código

public class ContadorCircular { public int getValor() {

return 0; }

}

//--- @Test

public void testVerValorDelContadorConValorInicial5() { ContadorCircular cc = new ContadorCircular(5); assertEquals(5, cc.getValor());

} // código

public class ContadorCircular { int valor; public ContadorCircular(int i) { this.valor = i; } public ContadorCircular() { this(0); }

public int getValor() { return this.valor; }

}

//--- @Test

public void testIncrementarContadorPorDefecto() { ContadorCircular cc = new ContadorCircular(); cc.incrementa();

assertEquals(1, cc.getValor()); }

// código

public void incrementa() { this.valor++;

}

//--- @Test

(7)

7

public void testIncrementarContadorDe5A10() {

ContadorCircular cc = new ContadorCircular(5, 5); cc.incrementa();

assertEquals(10, cc.getValor()); }

// código

public class ContadorCircular { int valor; int incremento; public ContadorCircular(int i) { this.valor = i; this.incremento = 1; } public ContadorCircular() { this(0); }

public ContadorCircular(int i, int j) { this(i);

this.incremento = j; }

public int getValor() { return this.valor; }

public void incrementa() {

this.valor+=this.incremento; }

}

//--- /* Refactorizamos

- Nombres de parámetros de constructores más descriptivos - Quitamos un constructor.

- Creamos los contadores en el setUp

- nombres más descriptivos para los contadores de pruebas */

public class TestContadorCircular { ContadorCircular ccPorDefecto; ContadorCircular ccCincoEnCinco; @Before

public void setUp() throws Exception {

ccPorDefecto = new ContadorCircular(); ccCincoEnCinco = new ContadorCircular(5, 5); }

@Test

public void testVerValorDelContadorPorDefecto() { assertEquals(0, ccPorDefecto.getValor()); }

@Test

public void testVerValorDelContadorConValorInicial5() { assertEquals(5, ccCincoEnCinco.getValor()); }

(8)

8

public void testIncrementarContadorPorDefecto() { ccPorDefecto.incrementa();

assertEquals(1, ccPorDefecto.getValor()); }

@Test

public void testIncrementarContadorDe5A10() { ccCincoEnCinco.incrementa();

assertEquals(10, ccCincoEnCinco.getValor()); }

}

// código

public class ContadorCircular { int valor;

int incremento;

public ContadorCircular(int valor, int incremento) { this.valor =valor; this.incremento = incremento; } public ContadorCircular() { this(0, 1); }

public int getValor() { return this.valor; }

public void incrementa() {

this.valor+=this.incremento; }

}

//--- @Before

public void setUp() throws Exception {

ccPorDefecto = new ContadorCircular(1); ccCincoEnCinco = new ContadorCircular(5, 5); }

@Test

public void testLimiteNoSuperadoContadorPorDefecto() { boolean b = this.ccPorDefecto.incrementa(); assertFalse(b);

}

// código int limite;

public ContadorCircular(int limite) { this(0, 1);

this.limite = limite; }

public boolean incrementa() { this.valor+=this.incremento; return false;

}

(9)

9

@Test

public void testLimiteSuperadoContadorPorDefecto() { this.ccPorDefecto.incrementa();

boolean b = this.ccPorDefecto.incrementa(); assertTrue(b);

}

// código

public boolean incrementa() { this.valor+=this.incremento; return this.valor > this.limite; }

//--- @Test

public void testLimiteSuperadoContadorDe5En5() { this.ccCincoEnCinco.incrementa();

boolean b = this.ccCincoEnCinco.incrementa(); assertTrue(b);

}

// código

public ContadorCircular(int valor, int incremento, int limite) { this.valor =valor;

this.incremento = incremento; this.limite = limite;

}

public ContadorCircular(int limite) { this(0, 1, limite);

}

//--- @Test

public void testContadorPorDefectoVuelvealValorInicialASuperarElLimite() { this.ccPorDefecto.incrementa(); this.ccPorDefecto.incrementa(); assertEquals(0, this.ccPorDefecto.getValor()); } // código int inicial;

public ContadorCircular(int valor, int incremento, int limite) { this.inicial = valor;

this.valor =valor;

this.incremento = incremento; this.limite = limite;

}

public boolean incrementa() { this.valor+=this.incremento;

boolean b = this.valor > this.limite; if (b) { this.valor = this.inicial; } return b; } //--- @Test

(10)

10

this.ccPorDefecto.incrementa(); this.ccPorDefecto.resetea(); assertEquals(0, this.ccPorDefecto.getValor()); } // código

public void resetea() {

this.valor = this.inicial; }

//--- /* Refactorizamos

- Evitamos código repetido */

public boolean incrementa() { this.valor+=this.incremento;

boolean b = this.valor > this.limite; if (b) {

this.resetea(); }

return b; }

Figure

Actualización...

Referencias

Actualización...

Related subjects :