Testing con JUnit 4

junit

El testing automatizado suele ser uno de los grandes olvidados en el proceso de desarrollo de software y la primera “víctima” de las desviaciones de presupuestos y plazos de entrega. Sin embargo es parte fundamental del proceso de creación de software, incluso existe una metodología (TDD) en la que el desarrollo se hace en base a los tests que debe verificar el software y no al revés, esto es, primero se diseñan los tests y luego la aplicación que los verifica.


Algunas ventajas del testing automatizado:

  1. Lo más evidente es que permite la detección de errores lo que aumenta la fiabilidad de la aplicación.
  2. Permite avanzar en la implementación de un proyecto o en la inclusión de nuevas funcionalidades asegurando que estos cambios no “rompen” funcionalidades ya existentes que funcionan correctamente. Utilizando un símil, supone programar con una red de seguridad.
  3. Una vez implementadas, las pruebas automatizadas se pueden ejecutar todas las veces que sean necesarias sin ningún coste adicional en comparación con las pruebas realizadas manualmente. Esto no implica que no se tenga que realizar testing manual sino que este se invierta en aquellas pruebas en las que aporte verdadero valor.
  4. Obliga a que el código de la aplicación sea razonablemente fácil de testear lo que implica el seguimiento de buenas prácticas como los principios SOLID. Este “efecto colateral” ayuda a aumentar la calidad del código facilitando su manteniemiento.

La clasificación de los tipos de test o pruebas que pueden realizarse es bastante extensa tal y como podemos ver en la Wikipedia. Como desarrolladores, generalmente vamos centrarnos en la implementación de pruebas unitarias que suelen suponer el grueso de los tests automatizados de una aplicación; aquí lo que probamos son unidades mínimas de código, generalmente métodos muy concretos, de forma exhaustiva y totalmente independiente del resto de la aplicación hasta tal punto que en muchos casos simularemos la interacción del código que probamos con otros componentes mediante mocks, stubs, dummys, etc

Gracias los tests unitarios a medida que vamos programando se valida el correcto funcionamiento del código a “bajo nivel” (lo cual por cierto no valida la funcionalidad de la aplicación final una vez que se unen todas las piezas).

JUnit 4

JUnit 4 es el framework de testing Open Source para Java más popular siendo TestNG la principal alternativa. Además ha sido adoptado por Google para hacer testing en Android, temática que abordaré próximamente en el blog.

A pesar de lo que su nombre sugiere, JUnit se puede utilizar para cualquier tipo de testing automatizado y no sólo para pruebas unitarias. JUnit proporciona una infraestructura básica sobre la que implementar nuestros tests en los que en muchos casos utilizaremos otras herramientas de testing como Mockito o Selenium o bien mecanismos de integración con los frameworks que utilicemos como por ejemplo Spring.

Los tests de JUnit 4 se implementan en clases POJO sin ninguna particularidad. No es necesario heredar de clase alguna ya que JUnit “conoce” los test a ejecutar en función de la anotaciones que encuentre (@Test). Estas clases pueden tener cualquier nombre, aunque se suele seguir la convención de utilizar el sufijo “Test”.

En el caso de Maven, estas clases se ubicarán en /src/test/java y los recursos que sean específicos de los tests estarán en el directorio /src/test/resources. En el ejemplo de este tutorial vamos a testear la siguiente clase:

package com.danielme.blog.testing;

import java.util.ArrayList;
import java.util.List;

public enum UserService {

    INSTANCE;

    private final String[] users = { "Bombur", "Bofur", "Bifur", "Nori", "Ori", "Dori", "Thorin",
            "Balin", "Dwalin", "Oin", "Gloin", "Fili", "Kili" };

    public List<String> getUsersByName(String name) {
        List<String> res = new ArrayList<String>();
        if (name != null) {
            for (String user : users) {
                if (user.toUpperCase().contains(name.toUpperCase())) {
                    res.add(user);
                }
            }
        }
        return res;
    }

    public String getUserByPosition(int pos) {
        return users[pos];
    }

}

En el pom debemos incluir la dependencia de JUnit. Utilizaremos el scope test para que JUnit, o cualquier otra dependencia con este scope, no se incluya en los artefactos finales del proyecto (jar, .war, .ear).

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

Al “ejecutarse” una clase con JUnit se invocarán los métodos que contengan las siguientes anotaciones (estos métodos deben ser públicos, no tener parámetros y devolver void):

  • @Test – cada método que implemente un test. Lo veremos más adelante.
  • @Before – se invoca siempre antes de la ejecución de cada @Test.
  • @After – se invoca siempre después de la ejecución de cada @Test.
  • @BeforeClass – método estático. Se invoca una única vez al ejecutarse la clase antes de cualquier método @Test o @Before. Por ejemplo, podríamos utilizar este método para arrancar una base de datos embebida como HSQLDB que usarán todos los tests y evitar el coste de realizar esta operación cada vez que se ejecute un test.
  • @AfterClass – método estático. Equivalente al anterior, pero ahora el método es el último que se invoca al ejecutarse la clase.
  • @Ignore – los test marcados con esta anotación no se ejecutarán.
  • Para los métodos anotados con @Before y @BeforeClass se suele seguir la convención de nombrarlos utilizando el prefijo setUp mientras que para los métodos anotados con @After y @AfterClass se utiliza el prefijo tearDown. Para los métodos @Test a veces se utiliza test como prefijo o sufijo aunque lo recomendable es utilizar nombres lo más descriptivo posible para facilitar la legibilidad de los tests.

    A modo de ejemplo, implementemos nuestra clase con los tests para UserService de tal modo que podamos ver detalladamente su flujo de ejecución:

    import org.apache.log4j.Logger;
    import org.junit.After;
    import org.junit.AfterClass;
    import org.junit.Before;
    import org.junit.BeforeClass;
    import org.junit.Test;
    
    public class UserServiceTest {
    
        private static final Logger logger = Logger.getLogger(UserServiceTest.class);
    
        @BeforeClass
        public static void setUpClass() {
            logger.info("");
        }
        
        @AfterClass
        public static void tearDownClass() {
            logger.info("");
        }
    
        @Before
        public void setUpTest() {
            logger.info("");
        }
    
        @After
        public void tearDownTest() {
            logger.info("");
        }
    
        @Test
        public void test1() {
            logger.info("");
        }
    
        @Test
        public void test2() {
            logger.info("");
        }
    
    }
    

    Podemos ejecutar todos los tests que tengamos definidos en el proyecto con mvn test (o una clase en concreto con mvn test -Dtest=clase), y en el directorio /target/surefire-reports/ encontraremos los informes con los resultados de la ejecución. Asimismo, al crearse el artefacto (el .jar. el .war…) del proyecto con package se ejecutarán todos los tests y si hay algún fallo o error el artefacto no se generará.

    Otra opción si estamos desarrollando es utilizar el soporte proporcionado por el IDE que utilicemos. Eclipse proporciona de serie integración con JUnit y podemos lanzar los tests pulsando sobre la clase con el botón derecho y seleccionando Run As -> JUnit test.

    UserServiceTest:17.setUpClass - 
    UserServiceTest:27.setUpTest - 
    UserServiceTest:37.test1 - 
    UserServiceTest:32.tearDownTest - 
    UserServiceTest:27.setUpTest - 
    UserServiceTest:42.test2 - 
    UserServiceTest:32.tearDownTest - 
    UserServiceTest:22.tearDownClass - 
    Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.165 sec
    

    Assertions

    Al escribir un test (a partir de ahora me referiré a test como a todo método anotado con @Test) tendremos que comprobar que los resultados que vamos obteniendo al invocar el código que estamos probando son los esperados.

    La ejecución de un test se considera exitosa si no hemos llamado a fail para informar que la ejecución del test ha sido fallida porque los resultados obtenidos no son los esperados. Si la ejecución del test no se puede completar y se aborta porque por ejemplo se lanza una excepción no controlada, el test se considera erróneo ya que directamente no ha podido ejecutarse.

    Nota: los métodos de la clase Assert se suelen utilizar con import estáticos, por ejemplo:

    import static org.junit.Assert.fail;
    

    Vamos a crear un test para probar el método getUsersByName. Haremos una búsqueda y comprobaremos que se devuelven exactamente los dos resultados esperados. No es una prueba muy buena que digamos pero nos permitirá ver cómo comprobar los resultados.

        @Test
        public void getUsersByName() {
            List<String> usersByName = UserService.INSTANCE.getUsersByName("oin");
            if (usersByName.size() != 2) {
                fail("size must be 2");
            } else if (!usersByName.contains("Gloin")) {
                fail("Gloin not found!!");
            }  else if (!usersByName.contains("Oin")) {
                fail("Oin not found!!");
            }
        }
    

    En caso de error, por ejemplo la lista obtenida no tiene exactamente 2 elementos, este será el resultado.

    getUsersByName(com.danielme.blog.testing.UserServiceTest)  Time elapsed: 0.005 sec  <<< FAILURE!
    java.lang.AssertionError: size must be 2
    	at org.junit.Assert.fail(Assert.java:88)
    	at com.danielme.blog.testing.UserServiceTest.getUsersByName(UserServiceTest.java:53)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:606)
    	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    	at org.junit.rules.ExpectedException$ExpectedExceptionStatement.evaluate(ExpectedException.java:239)
    	at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298)
    	at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292)
    	at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    	at java.lang.Thread.run(Thread.java:745)
    
    
    Results :
    
    Failed tests:   getUsersByName(com.danielme.blog.testing.UserServiceTest): size must be 2
    

    Para facilitar las comprobaciones de resultados JUnit pone a nuestra disposición una amplia colección de métodos en la clase Assert que realizan validaciones sobre sus parámetros de entrada de tal modo que si la validación no se cumple el test fallará de forma automática, esto es, no tendremos que invocar el fail.

    En el caso de los test unitarios, es recomendable que sólo tengamos un assert en cada método de test para asegurar que estamos probando un único comportamiento que sólo puede fallar por un motivo. Esto implica que en numerosas ocasiones para probar un mismo método sea necesario implementar varios tests. En el ejemplo vamos a usar más de un assert porque queremos validar más de una condición sobre un mismo resultado.

    Podemos reescribir nuestro test tal que así:

        @Test
        public void getUsersByName() {
            List<String> usersByName = UserService.INSTANCE.getUsersByName("oin");
            assertEquals(2, usersByName.size());
            assertTrue("Gloin not found!!", usersByName.contains("Gloin"));
            assertTrue("Oin not found!!", usersByName.contains("Oin"));        
        }
    

    Es una buena práctica utilizar las versiones de los métodos assert que admiten un texto de mensaje, aunque en el caso concreto de assertEquals se ha omitido el mensaje de fallo porque el mensaje por defecto es lo bastante descriptivo:

    Failed tests:   getUsersByName(com.danielme.blog.testing.UserServiceTest): expected: 2 but was: 3
    

    Parametrización

    Para hacer pruebas exhaustivas necesitaremos con frecuencia ejecutar varias veces el mismo test pero cambiando cada vez algunos parámetros de entrada (y probablemente también el resultado esperado). JUnit permite la creación de tests parametrizados pero el mecanismo que proporciona es algo engorroso y poco flexible. En su lugar prefiero utilizar la librería JUnitParams, muy práctica y fácil de utilizar.

    La incluimos en nuestro pom.xml.

    <dependency>
      <groupId>pl.pragmatists</groupId>
      <artifactId>JUnitParams</artifactId>
      <version>1.0.5</version>
      <scope>test</scope>
    </dependency>
    

    Ahora, a modo de ejemplo vamos a implementar un test sobre el método getUsersByName que se ejecutará 3 veces cambiando cada vez la cadena de búsqueda y el resultado esperado.

    @RunWith(JUnitParamsRunner.class)
    public class UserServiceTest {
    ...
    
        @Test
        @Parameters({"oin, 2", "balin, 1", "gimli, 0" })
        public void getUsersByNameParameterised(String name, int result) {
            List<String> usersByName = UserService.INSTANCE.getUsersByName(name);
            assertEquals(result, usersByName.size());        
        }
    

    Si fallara algún test la ejecución del array de parámetros no se detendría y siempre se ejecuta todo el juego de parámetros.

    En Eclipse podemos ver claramente cómo se va ejecutando el test parametrizado.

    JUnitParams eclipse

    Excepciones

    En ocasiones el resultado que queremos validar en un test es el lanzamiento de una excepción. Esto podemos hacerlo de tres formas distintas:

    • Manualmente con try-catch y fail.

          @Test
          public void getUserByPositionException1() {
              try {
                  UserService.INSTANCE.getUserByPosition(-1);
                  fail("Exception not thown");
              } catch (IndexOutOfBoundsException ex) {
                 assertEquals("-1", ex.getMessage());
              }
          }
      
    • Indicando la excepción que se debe lanzar en la anotación @Test.
          @Test(expected = IndexOutOfBoundsException.class)
          public void getUserByPositionException2() {
              UserService.INSTANCE.getUserByPosition(-1);
          }
      
    • De forma más sofisticada utilizando el mecanismo de reglas de JUnit que permite modificar el comportamiento de los tests. Usaremos la regla ExpectedException ya incluida en JUnit del siguiente modo:
      1. Definir la regla:

            @Rule
            public ExpectedException exception = ExpectedException.none(); 
        
      2. Indicar al inicio del test la excepción esperada. Opcionalmente también podemos validar el mensaje de la excepción (hay varios métodos para hacerlo).
            @Test
            public void getUserByPositionException3() {
                exception.expect(IndexOutOfBoundsException.class);
                exception.expectMessage("-1");
             
                UserService.INSTANCE.getUserByPosition(-1);
            }
        

    Timeout

    JUnit permite definir para cada test un timeout o tiempo máximo de ejecución en milisegundos. Si pasado ese tiempo el test no ha finalizado su resultado será fallido.

    El timeout se configura de forma similar a la validación de excepciones:

    • Con la anotación @Test : @Test(timeout=5000)
    • Con una regla que se aplicará a cada uno de los test de la clase.
    •     
      @Rule
      public Timeout timeout = Timeout.millis(5000);
      

      Esta regla tiene en consideración el tiempo de ejecución de los métodos @Before y @After que acompañan a cada test.

    Orden de ejecución

    Una de las preguntas más frecuentes que suelen realizar los neófitos con JUnit es cómo se puede establecer el orden de ejecución de los tests. El planteamiento es erróneo ya que un test ha de ser totalmente autónomo y nunca depender de la ejecución de otro. Por tanto no debería ser necesario ejecutar los tests siempre en el mismo orden.

    A pesar de esto, con la anotación @FixMethodOrder es posible forzar que los tests definidos en una clase se ejecuten siempre en orden alfabético.

    Suite de tests

    Una Suite consiste en una agrupación de varias clases de tests, como la que hemos implementado en este tutorial, para que se ejecutan de forman conjunta. Por ejemplo, podemos crear una suite con los tests que prueban la capa de negocio, otra con las pruebas de integración, etc. De esta forma sólo necesitamos lanzar la ejecución de la suite para que se ejecuten todos los tests que la componen.

    Definir una suite es muy sencillo ya que sólo hay que crear una clase y anotarla indicando las clases de tests que componen la suite. El orden de ejecución de estas clases será el que indiquemos. Por ejemplo:

    package com.danielme.testing;
    
    import org.junit.runner.RunWith;
    import org.junit.runners.Suite;
    
    @RunWith(Suite.class)
    @Suite.SuiteClasses({
        UserService.class,
        StoreService.class
    })
    public class ServiceSuite {
    //clase vacia
    }
    


    Código de ejemplo

    El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: