Tema 3. Test Driven Development Ejercicios Resueltos

Anuncio
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
static class CrivaDeEratosthenes {
1
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;
}
2
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.
3
@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:
4
@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.
5
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
6
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());
}
@Test
7
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;
}
//----------------------------------------------------
8
@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
public void testResetearContadorPorDefecto() {
9
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;
}
10
Descargar