Ahora que hemos visto la importancia de hacer test automáticos de pruebas en nuestros programas más o menos serios, que requieren de un tiempo largo y en el que participan varios desarrolladores, vamos a ver un pequeño ejemplo con la librería JUnit de java, una librería que nos ayudará en la realización de nuestros test automáticos de prueba. Ojo, la librería no codifica automáticamente los test, eso tienes que hacerlo tú, pero la librería te ayuda a presentar los resultados y ejecutar los test.
Para realizar un test automático de prueba, debemos realizar una clase o un trozo de código que se encargue de hacer new de las clases que queremos probar y que se encargue de llamar a los métodos de dichas clases, pasando parámetros concretos y viendo que el resultado es el esperado. Es posible que dicho resultado nos venga por el return del método, o es posible que ese método no devuelva nada, pero haga algo en otro sitio y es ahí donde debemos ir a verificar si se ha hecho. Por ejemplo, un método suma() al que se pasan dos parámetros, devuelve un resultado, este método es fácil de testear. Un método insertaEnBaseDeDatos() al que se pasan varios parámetros, no devuelve nada (quizás un true o false si la operación ha tenido éxito), pero nuestro programa de pruebas tendrá que ir a la base de datos y comprobar que se ha hecho la inserción.
Nuestro programa de prueba puede ser simplemente una clase con un main() que haga eso y que de alguna forma devuelva si la clase funciona o no. Un buen programa de prueba automático NO debe darnos largos listados en los que nosotros analizamos manualmente si los resultados son los esperados o no. Tampoco debe requerir que pulsemos botones, pulsemos <intro> o hagamos cualquier otra cosa mientras se hace la prueba. Un buen programa automático de prueba debe arrancarse de forma simple, debe pasar él solo todas las pruebas y únicamente nos debe decir si todas están bien o no. En caso de estar mal, sí puede (y debe) darnos información de qué prueba es la que ha fallado.
Como ejemplo, vamos a hacer un par de clases tontas, una clase Suma.java y otra clase Resta.java. Ambas tienen dos métodos, la primera getSuma() e incrementa(), la segunda getDiferencia() y decrementa(). En los enlaces puedes ver el código completo de estas clases.
Vamos a usar JUnit para hacer los test de estas clases y de sus métodos. Las versiones 3.8.1 y 4.5 de JUnit sirven para lo mismo y hacen lo mismo, pero de distinta manera, así que empezaremos con JUnit 3.8.1 y luego contaremos las diferencias con la 4.5
Para nuestros programas de prueba podemos hacer una o más clases de prueba, va en gustos, pero lo normal es hacer una clase de prueba por cada clase a probar o bien, una clase de prueba por cada conjunto de pruebas que esté relacionado de alguna manera. Nosotros tenemos dos clases; Suma y Resta, así que haremos dos clases de prueba TestSuma y TestResta. No es necesario llamar a estas clases con Test***, pero sí es conveniente seguir algún tipo de criterio por dos motivos:
En el caso de JUnit 3.8.1, debemos hacer que estas clases de prueba hereden de la clase TestCase de JUnit. Para JUnit un TestCase es una clase de test. Tenemos entonces, dos clases TestSuma y TestResta, que van a heredar ambas de TestCase
import junit.framework.TestCase
...
public class TestSuma extends TestCase {
...
}
Ahora tenemos que hacer los métodos de test. Cada método probará alguna cosa de la clase. JUnit 3.8.1 requiere que estos métodos empiecen por test***. En el caso de TestSuma, vamos a hacer dos métodos de test, de forma que cada uno pruebe uno de los métodos de la clase Suma.
Por ejemplo, para probar el método getSuma() de la clase Suma, vamos a hacer un método de prueba testGetSuma(). Este método sólo tiene que instanciar la clase Suma, llamar al método getSuma() con algunos parámetros y comprobar que el resultado es el esperado.
import junit.framework.TestCase
...
public class TestSuma extends TestCase {
Suma suma = new Suma();
double resultado = suma.getSuma(1.0, 1.0);
// Aqui debemos comprobar el resultado
}
El ejemplo es muy tonto, porque vamos a sumar 1 más 1, que ya sabemos que a veces devuelve 2. ¿Cómo comprobamos ahora que el resultado es el esperado?. Al heredar de TestCase, tenemos disponibles de esta clase padre un montón de métodos assert***. Esto métodos son los que nos permiten comprobar que el resultado obtenido es igual al esperado. Uno de los métodos más usados es assertEquals(), que en general admite dos parámetros: El primero es el valor esperado y el segundo el valor que hemos obtenido. Así, por encima, la comprobación del resultado podría ser tan simple como esto
import junit.framework.TestCase
...
public class TestSuma extends TestCase {
Suma suma = new Suma();
double resultado = suma.getSuma(1.0, 1.0);
assertEquals(2.0, resultado);
}
Sin embargo, tenemos variantes de este método con más parámetros que pueden ser interesantes. Todos los métodos assertEquals() pueden admitir un primer parámetro String, para que pongamos un pequeño texto que identifique el tipo de test que estamos haciendo
assertEquals("1+1 deberia ser 2", 2.0, resultado);
y para el caso concreto de double o float, que es nuestro caso, como sabemos que los ordenadores suelen empeñarse en que 1+1 es 1.999999999 o bien 2.00000001, podemos añadir un cuarto parámetro indicando cuánto es el error que permitimos que, por supuesto, debería ser muy pequeño
assertEquals("1+1 deberia ser casi 2", 2.0, resultado, 1e-6);
Finalmente, indicar que hay muchos más métodos assert*** para otras cosas, como assertNotNull() que indica que el resultado esperado no debe ser null, assertTrue(), que indica que el resultado esperado debe ser true, etc. Estos, por supuesto, no necesitan que se introduzca por parámetro el resultado esperado, es decir, assertTrue() no necesita que le pasemos un parámetro con true.
assertTrue(resultado); // correcto
assertTrue(true, resultado); // innecesario.
Si hacemos ahora el test del método incrementa() de la clase Suma, haremos un método testIncrementa() en la clase TestSuma. Nuevamente, debemos hacer un new de la clase Suma, llamar al método y comprobar los resultados
public void testIncrementa()
{
Suma suma = new Suma();
double resultado = suma.incrementa(1.0);
assertEquals("Al incrementar 1 deberia dar 2", 2.0, resultado, 1e-6);
}
Vemos que en ambos test hemos repetido algo, el new de de la clase Suma. En este ejemplo sólo es una línea de código y no es mucho trabajo, pero imagina que en cada test tienes que instanciar varias clases y pasarles determinados parámetros en el constructor o a través de métodos set() antes de poder pasarles el test. Habría mucho código repetido en todos los test. Por ello, en las clases de test que heredan de TestCase de JUnit, podemos sobreescribir el método setUp() de la clase padre. JUnit se encargará de llamar a ese método antes de cada test. Podemos, entonces, hacer que la clase TestSuma tenga un atributo Suma y en el método setUp() encargarnos de inicializarlo correctamente en cada caso.
Al igual que puede ser necesario hacer algo antes de los test, a veces también es necesario hacer algo después de los test, como cerrar conexiones, borrar ficheros que haya generado el test, etc. Nuevamente la clase TestCase de JUnit se encarga de llamar a un método que podemos sobreescribir, tearDown().
En TestSuma.java y TestResta.java tienes cómo quedarían al final las clases, haciendo los dos test y haciendo el new de la clase bajo prueba en el método setUp().
Una vez que tenemos los test, hay que ejecutarlos. No tenemos clase main en ellos, así que tendríamos que hacer una clase con main para ejecutar estos test, haciendo el new de la clase de test y llamando a los métodos en el orden correcto., además de recoger de alguna forma los resultados. Pero afortunadamente JUnit 3.8.1 nos proporciona un par de clases con este main ya hecho, la primera lo hace todo en modo texto, la segunda nos muestra una ventana con los resultados. Estas clases son junit.textui.TestRunner y junit.swingui.TestRunner. Para llamarlas desde línea de comandos debemos poner el CLASSPATH apuntando tanto a nuestro programa como al jar de JUnit, ejecutar la clase TestRunner elegida y poner como parámetros las clases de Test que queramos ejecutar.
C:\> set CLASSPATH=path\mi.jar;path\junit.jar
C:\> java junit.textui.TestRunner com.chuidiang.ejemplos.junit38.TestResta
..
Time: 0,006
OK (2 tests)
O bien, si ejecutamos TestSuma, en el que deliberadamente hemos introducido fallos para que salte, obtenemos
C:\> set CLASSPATH=path\mi.jar;path\junit.jar
C:\> java junit.textui.TestRunner com.chuidiang.ejemplos.junit38.TestSuma
Time: 0,007
There was 1 failure:
1) testGetSuma(com.chuidiang.ejemplos.junit38.TestSuma)junit.framework.AssertionFailedError: Test suma expected:<2.0> but was:<1.0>
at com.chuidiang.ejemplos.junit38.TestSuma.testGetSuma(TestSuma.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at com.chuidiang.ejemplos.junit38.Main.main(Main.java:21)
FAILURES!!!
Tests run: 2, Failures: 1, Errors: 0
en el que vemos que el método testGestSuma() ha dado un error, se esperaba un 2.0 pero se ha obtenido un 1.0.
Si ejecutamos con junit.swingui.TestRunner, obtendremos una ventana tan bonita como esta:
C:\> set CLASSPATH=path\mi.jar;path\junit.jar
C:\> java junit.swingui.TestRunner com.chuidiang.ejemplos.junit38.TestSuma
En el menú superior aparecerán todas las clases de Test que hayamos pasado como parámetros. Seleccionando una y pulsando "Run" podemos ejecutarla. La barra roja representa que hay un test en fallo. Si todo fuera correcto saldría verde. Vemos en medio la clase TestSuma con sus métodos de test, marcados en verde o con una cruz según hayan sido correctos o no. Si seleccionamos testGetSuma() que ha dado fallo, veremos en la caja inferior el motivo del fallo.
Ejecutar los test uno a uno es un poco pesado, sobre todo si tenemos en cuenta que la verdadera utilidad de estos test es ejecutarlos con frecuencia, según vamos añadiendo funcionalidad a nuestro código para asegurarnos que lo que estaba funcionando sigue funcionando. Por ello, necesitamos algo de más alto nivel, algo que sepa todos los test que hay que ejecutar y que lo haga. Para esto está la clase TestSuite. Esta clase de JUnit se configura pasándole todos los nombres de clases de test que queremos que se pasen. JUnit se encargará, pasándole esta clase TestSuite, de ir ejecutando todos los test que haya en ella.
Para usar TestSuite, debemos crear una clase cualquiera que tenga un método estático como el siguiente:
import junit.framework.Test;
public class AllTests {
public static Test suite() {
TestSuite suite = new TestSuite("Test for com.chuidiang.ejemplos");
suite.addTestSuite(TestResta.class);
suite.addTestSuite(TestSuma.class);
return suite;
}
}
En el método creamos una clase TestSuite, pasándole en el constructor un texto que saldrá luego en los resultados. A esa TestSuite, le vamos llamando a addTestSuite() pasándole cada una de nuestras clases de Test. Finalmente, devolvemos esa TestSuite en el return.
Esta clase podemos ahora ejecutarla con uno de los TestRunner de JUnit como si fuera un test normal, pero se iran ejecutando todos los test que hay dentro. Ahora, ejecutar todos los test de golpe, es más sencillo.
C:\> set CLASSPATH=path\mi.jar;path\junit.jar
C:\> java junit.textui.TestRunner com.chuidiang.ejemplos.junit38.AllTests
Time: 0,006
There was 1 failure:
1) testGetSuma(com.chuidiang.ejemplos.junit38.TestSuma)junit.framework.AssertionFailedError: Test suma expected:<2.0> but was:<1.0>
at com.chuidiang.ejemplos.junit38.TestSuma.testGetSuma(TestSuma.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at com.chuidiang.ejemplos.junit38.Main.main(Main.java:21)
FAILURES!!!
Tests run: 4, Failures: 1, Errors: 0
Vemos al final que se han ejecutado cuatro test (dos métodos de TestSuma y otros dos de TestResta) y que ha fallado uno. En la traza vemos que ha sido, como ya sabíamos, testGetSuma().
A partir de la versión 4.0 de JUnit, hay pequeñas grandes diferencias a la hora de hacer los test. Se hace todo exactamente igual, con la excepción que se usan anotaciones de java en vez de tener que heredar de determinadas clases o cumplir determinada convención de nombres. Vamos aquí nada más a centrarnos en los detalles, ya que no sería demasiado útil repetir todas las explicaciones del apartado anterior.
Para hacer una clase de Test, en JUnit 3.8.1 debíamos heredar de TestCase. En JUnit 4.5 ya no es necesaria esta herencia, cualquier clase puede ser de test. Al igual que con JUnit 3.8.1, no hay ninguna obligación en el nombre de la clase de test, pero sí es conveniente que nosotros sigamos algún tipo de nomenclatura, como hacer que todas las clases de test empiecen por Test***, por los mismos motivos mencionados anteriormente.
Los métodos de test de estas clases, ya no tienen que empezar por test***() como obligaba JUnit 3.81. Ahora hay que ponerles una anotación @Test. Así, nuestro método testGetSuma(), se puede cambiar de nombre y quedaría
@Test
public void aVerSiSumaBien() {
...
}
Antes accediamos a los métodos assertEquals() y demás porque los heredábamos de la clase padre TestCase. Ahora estos métodos son métodos estáticos de la clase org.junit.Assert, por lo que podemos importar dicha clase y usar sus métodos
import org.junit.Assert
...
Assert.assertEquals("1+1 deberian ser 2", 2.0, resultado, 1e-6);
o bien podemos hacer un static import para usar los métodos directamente
import static org.junit.Assert.*;
...
assertEquals("1+1 deberian ser 2", 2.0, resultado, 1e-6);
Los métodos setUp() y tearDown() ya no tienen que llamarse así, pueden ser don métodos cualesquiera. Unicamente tendremos que ponerles las anotaciones @Before para setUp() y @After para tearDown()
@Before
public void paraEjecutarAntes() throws Exception {
resta = new Resta();
}
@After
public void paraEjecutarDespues() throws Exception {
// Liberar recursos
}
En TestSuma.java y TestResta.java tienes como quedarían ahora esos métodos de test, usando JUnit 4.5
Para la TestSuite, nos basta una clase cualquiera, incluso aunque no tenga métodos. Basta con anotarla con @RunWith(Suite.class) para que JUnit sepa que debe ejecutar esa clase como una TestSuite, y anotarla también con @SuiteClasses( { TestResta.class, TestSuma.class }) pasando las clases de test. Es decir, algo como esto
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { TestResta.class, TestSuma.class })
public class AllTest {
}
Para ejecutar esta clase (o cualquiera de los test individuales), en JUnit 4.5 tenemos la clase org.junit.runner.JUnitCore, a la que pasamos como parámetro la clase de TestSuite
C:\> set CLASSPATH=path\mi.jar;path\junit.jar
C:\> java org.junit.runner.JUnitCore com.chuidiang.ejemplos.junit45.AllTests
JUnit version 4.5
....E
Time: 0,022
There was 1 failure:
1) aVerSiSumaBien(com.chuidiang.ejemplos.junit45.TestSuma)
java.lang.AssertionError: Test suma expected:<2.0> but was:<1.0>
at org.junit.Assert.fail(Assert.java:91)
at org.junit.Assert.failNotEquals(Assert.java:618)
at org.junit.Assert.assertEquals(Assert.java:414)
at com.chuidiang.ejemplos.junit45.TestSuma.aVerSiSumaBien(TestSuma.java:32)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
at org.junit.runners.ParentRunner.run(ParentRunner.java:220)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:116)
at org.junit.runner.JUnitCore.run(JUnitCore.java:107)
at org.junit.runner.JUnitCore.runMain(JUnitCore.java:88)
at org.junit.runner.JUnitCore.runMainAndExit(JUnitCore.java:54)
at org.junit.runner.JUnitCore.main(JUnitCore.java:46)
FAILURES!!!
Tests run: 4, Failures: 1
Vemos al final que ha ejecutado cuatro métodos de test y que ha fallado uno, el ya consabido aVerSiSumaBien().
Vamos ahora a ver cómo se integra JUnit con herramientas como eclipse o maven.