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