Primeros pasos con JUnit 5

0
21275

Índice de contenidos

Logo de Junit 5



1. Introducción

Junit 5 es la nueva versión del conocidísimo framework para la automatización de pruebas en Java. Ahora mismo se encuentra en su Milestone 2 (publicada el 23 julio de 2016), con lo que no esperamos que haya cambios significativos de aquí a su lanzamiento definitivo, simplemente corrección de bugs y cosas menores.

Esta versión 5 se apoya mucho en las novedades de Java 8, cómo son las lambdas, y en este tutorial vamos a hacer un repaso por sus principales características y empezar a descubrir las novedades que trae.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15» (2.5 GHz Intel i7, 16GB 1600 Mhz DDR3, 500GB Flash Storage).
  • AMD Radeon R9 M370X
  • Sistema Operativo: Mac OS X El Capitan 10.11.6
  • Java v1.8.0_102
  • Maven v3.3.9
  • Gradle v3.0
  • JUnit v5-M2

3. Nombrado de los tests

@DisplayName("Aserciones soportadas")
class AssertionsTest {

    @Test
    @DisplayName("Verdadero o falso ?")
    void true_or_false() {
        ...
    }

    @Test
    @DisplayName("Existencia de un objeto")
    void object_existence() {
        ...
    }
}

Igual que en JUnit 4, las clases de test acaban con el sufijo Test. Y también marcamos los métodos de test con la anotación @Test. Ojo aquí porque Junit 5 tiene su propia anotación org.junit.jupiter.api.Test así que ojo con no poner la de JUnit 4 porque en tal caso el runner de JUnit 5 no ejecutará estos tests.

También llama la atención la posibilidad de usar la anotación @DisplayName para indicar el nombre del test mediante un texto libre (como cosa curiosa vemos que podemos usar emoticonos dentro del @DisplayName).

Esto nos sirve para que las herramientas presenten este texto en lugar del nombre del método.
 

JUnit 5 @DisplayName in IntelliJ

 

Para mi la verdad es que esta anotación no aporta demasiado valor, ya que con el nombre del método debería ser más que suficiente. Si además del nombre del método ponemos la anotación, en cierto modo estamos duplicando información, rompiendo así el principio DRY. Otra opción sería poner el @DisplayName y como nombre del tests poner sólo t1(), t2(), …​ pero esto tampoco parece que tenga sentido ya que en muchos sitios lo que se va a mostrar es sólo el nombre del método por lo que sería muy poco descriptivo.

Para mi lo mejor es no usar la anotación @DisplayName y poner nombres que indiquen la intención del tests, cómo hacerReserva(), tramitarPedido(), …​ Si queremos que los nombres sean un poco más legibles podemos usar guiones bajos: hacer_reserva(), tramitar_pedido(), …​

Para mi los nombres de los tests han de ser lo más genéricos posibles para que podamos refactorizar el test y no tengamos que cambiar el nombre. Es decir, siempre intento evitar poner detalles de, por ejemplo, en qué consiste la fixture, o qué valores concretos voy comprobar en las aserciones. Aunque esto en principio puede parecer buena idea, a la larga lo que conseguimos es que estas cosas cambien y que el nombre del test, como buena documentación qué es, acabe mintiendo. Además no nos engañemos, cuando un test falla, el nombre te debe dar una idea de lo que está ocurriendo, pero siempre vamos a ir al código del test a ver exactamente qu´ es lo que se está haciendo.

4. Mejora en el diseño de las clases de test

Una cosa que puede pasar desapercibida del código anterior es que ni la clase y ni los métodos de test necesitan ser públicos (public).

Esto no tiene un impacto directo en la forma en la que escribimos los tests, pero sí es una mejora desde el punto de vista del diseño y de la semántica de los propios tests, ya que estas clases de tests no están concebidas para ser instanciada o para llamar a sus métodos, si no es por el propio framework de JUnit.

5. Ciclo de vida

class LyfecicleTest {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

    @Test
    void regular_testi_method() {
        ...
    }

    @Test
    @Disabled("este tests no se ejecuta")
    void skippedTest() {
        // not executed
    }

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }
}

Vemos que tenemos la mismas anotaciones que con JUnit 4 para determinar qué métodos se tienen que ejecutar antes y después de todos o cada uno de los test.

La única diferencia con la versión anterior es la forma de hacer que un test no se ejecute, mientras en la versión 4 era con @Ignore, ahora en la versión 5 lo haremos con @Disabled, como podemos ver en la línea 17.

6. Aserciones

class AssertionsTest {

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "Ahora el mensaje opcional de la aserción es el último parámetro.");
        assertTrue(2 == 2, () -> "Al usar una lambda para indicar el mensaje, "
                + "esta se evalúa cuando se va a mostrar (no cuando se ejecuta el assert), "
                + "de esta manera se evita el tiempo de construir mensajes complejos innecesariamente.");
    }

    @Test
    void groupedAssertions() {
        // En un grupo de aserciones se ejecutan todas ellas
        // y ser reportan todas los fallos juntos
        assertAll("user",
            () -> assertEquals("Francisco", user.getFirstName()),
            () -> assertEquals("Pérez", user.getLastName())
        );
    }

    @Test
    void exceptionTesting() {
        Throwable exception = expectThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }
}

Disponemos de los mismos Assert de JUnit 4, con una algunas peculiaridades, como que ahora tanto la expresión como el mensaje del Assert, se pueden indicar mediante una lambda de Java 8. Se puede ver un ejemplo en la línea 7 o 17.

Otra novedad es que podemos indicar grupos de Asserts mediante assertAll. La gracia de estos grupos de Asserts es que siempre se ejecutan todos, y que luego los fallos que se hayan producido también se van a reportar de forma conjunta.

Otro cambio es la forma hacer aserciones sobre excepciones. Como podemos ver en la línea 23, ahora en vez de usar @Expected usaremos el método expectThrows().

La ventaja de este método es que es más flexible ya que tenemos el control total de la excepción dentro del método de test, por lo que podemos hacer más comprobaciones que antes (por ejemplo antes con el @Expected no podíamos validar el mensaje de la excepción).

Además este estilo de capturar las excepciones está más acorde con las buenas prácticas de testing donde lo que se recomienda es que la última línea de un tests siempre sea un Assert (esto no ocurría al usar la anotación @Expected).

¡Ojo, ya no hay soporte directo para Hamcrest!

Efectivamente si inspeccionamos el API ya no encontraremos rastro del método assertThat. La principal motivación de quitar esto ha sido para eliminar la dependencia que JUnit tenía con esta librería, que en algunas ocasiones podía incluso provocar algún conflicto en la resolución de clases (en particular cuando teníamos otras librerías que incluían una versión diferente de Hamcrest).

Si ahora queremos seguir usando Hamcrest tendremos que usar el método MatcherAssert.assertThat() que proporciona el propio Hamcrest.

7. Asunciones

Las asunciones nos permiten decidir si un test se debe de ejecutar o no en función de alguna condición, por ejemplo si estamos en el entorno de desarrollo o de integración, o si estamos en una máquina Windows o Unix. Esto ya lo teníamos en JUnit 4, pero ahora podemos usar lambdas de Java 8 para las expresiones.

class AssumptionsDemo {

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));

        // Resto del tets, sólo se ejecutará si estamos en el entorno de "CI"
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Abortando test: no estamos en una máquina de desarrollo");

        // Vemos como podemos usar una lambda para indicar el mensaje.
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // Esta aserción sólo se ejecuta en el entorno de "CI"
                assertEquals(2, 2);
            });

        // Esta aserción se ejecuta en todos los entornos
        assertEquals("a string", "a string");
    }
}

8. Etiquetas

@Tag("fast")
@Tag("model")
class TaggingDemo {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
    }
}

En JUnit 4 el equivalente a las etiquetas son las Categories. Esto nos va a permitir lanzar conjuntos específicos de tests en función de las etiquetas que especifiquemos.

9. Clases anidadas

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    void is_instantiated_with_new() {
        new Stack<>();
    }

    @Nested
    class WhenNew {

        @BeforeEach
        void create_new_stack() {
            stack = new Stack<>();
        }

        @Test
        void is_empty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        void throws_exception_when_popped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        void throws_exception_when_peeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        class AfterPushingAnElement {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            void is_is_no_longer_empty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            void return_element_when_popped_and_is_empty() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            void return_element_when_peeked_but_remains_not_empty() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

Las clases de test anidadas nos permiten crear una estructura jerárquica para la ejecución de nuestros tests, de forma que podemos compartir fixtures y precondiciones (en JUnit 4 podíamos hacer algo parecido con runner Enclosed.class). El ejemplo anterior se leería de la siguiente manera:

TestingAStackDemo
    is_instantiated_with_new

    WhenNew
        is_empty
        throws_exception_when_popped
        throws_exception_when_peeked

        AfterPushingAnElement
            is_is_no_longer_empty
            return_element_when_popped_and_is_empty
            return_element_when_peeked_but_remains_not_empty

Nótese como podemos no tenemos límite en el nivel de anidación de las clases. También cabe destacar que las clases anidadas no pueden ser estáticas.

10. Conclusiones

Hemos hecho un repaso por las principales novedades que trae JUnit 5, y si bien los cambios no son radicales, sí que encontramos una serie de cosas que nos pueden facilitar una poco nuestro trabajo diario.

Os animo a que, después de esta introducción a las principales novedades, le echéis un vistazo a:

  • Escribir tests para interfaces usando los métodos por defecto que se pueden implementar en las interfaces de Java 8
  • Generación de tests dinámicamente con @TestFactory
  • @ExtensWith para indicar puntos de extensión del propio framework (por ejemplo para enganchar Mockito)

Después de esto sólo nos queda ir practicado y esperar a que publiquen la versión final para poder incorporarla en nuestros proyectos. Por ahora los planes son que esté lista para finales del 2016, así que, si toda va bien, la publicación de la GA (General Availability) debería ser inminente.

11. Sobre el autor

Alejandro Pérez García (@alejandropgarci)
Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster

Socio fundador de Autentia Real Business Solutions S.L. – «Soporte a Desarrollo»

Socio fundador de ThE Audience Megaphone System, S.L. – TEAMS – «Todo el potencial de tus grupos de influencia a tu alcance»

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad