Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos en memoria.

Última actualización: 12/08/2022

logo springEn el tutorial Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA se construye una pequeña API REST para una tabla de una base de datos MySQL a la que se accede a través de Spring Data JPA.

No obstante, falta un elemento vital: las pruebas automáticas. Para suplir esta carencia, en el presente tutorial voy a mostrar cómo configurar tests de integración en Spring Boot -requieren iniciar el contexto de Spring- utilizando esa misma aplicación y algunas opciones y librerías que pueden ser de gran ayuda. Usaremos la versión 2.7 de Spring Boot y señalaré cuando sea oportuno algunas diferencias relevantes con respecto a versiones anteriores.

El siguiente diagrama de clases ofrece una visión general de la estructura de la aplicación de ejemplo (click para ampliar).



¿JUnit 4 o 5?

Mucho ha llovido desde la primera publicación de este tutorial a finales de 2018. Originalmente, estaba orientado al uso de JUnit 4 y se acompañaba de las explicaciones pertinentes para emplear JUnit 5 que, por aquel entonces, era algo novedoso. Aprovecho para subrayar que no es una nueva versión de JUnit, sino una plataforma sobre la que construir librerías de testing en Java. Incluye una denominada JUnit Jupiter que, en la práctica, es lo que se conocen popularmente como tests de JUnit 5. Todo esto lo explico aquí.

En la actualidad, JUnit 5 es ampliamente utilizado, quedando JUnit 4 relegado a proyectos ya existentes en los que la actualización no se considere oportuna o viable. De hecho, es la plataforma de pruebas predeterminada en Spring Boot desde la versión 2.2 (octubre 2019). Así pues, en la versión actual del tutorial he decidido seguir el enfoque contrario del cual partí: el código se basa en JUnit 5, y daré las instrucciones para poder usar JUnit 4.

Escribiendo la primera prueba

Vamos a añadir una prueba con JUnit 5 (Jupiter) que haga uso de clase CountryService.

  1. Se requiere el starter spring-boot-starter-test para tests.
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    

    Desde la versión 2.2, Spring Boot incluye JUnit 5. En versiones anteriores, se precisa esta dependencia.

    <dependency> 
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <scope>test</scope>
    </dependency> 
    
  2. Creamos la clase que contendrá las pruebas y la marcamos, de momento, con dos anotaciones:
    • Indicamos que queremos ejecutar las pruebas con la extensión SpringExtension. El mecanismo de extensiones es una de las grandes novedades de JUnit 5. Permite intervenir en el ciclo de ejecución de las pruebas para incluir nuestras propias tareas. En el caso de Spring, la extensión ya existente es responsable, grosso modo, de crear y destruir el contexto de Spring para que podamos probar sus beans.

      Cuando usamos Spring con Spring Boot, esta anotación no es necesaria por estar implícita en @SpringBootApplication y en otras similares que acotan el ámbito del test como @DataJpaTest o @WebMvcTest. Todas ellas las veremos en este tutorial.

    • Con @SpringBootTest levantamos el contexto de Spring al completo partiendo de una clase anotada con @SpringBootApplication.
      Spring la busca desde el paquete en el que se encuentre la clase con las pruebas y hacia «arriba» -siguiendo la jerarquía de paquetes- tomando la primera que encuentre. Si hay varias y no queremos que se aplique este comportamiento, se pueden especificar las que usaremos con el atributo classes de @SpringBootTest. En él también podemos proporcionar otras clases de configuración (@Configuration) adicionales, aunque, como veremos más adelante, para crear configuraciones exclusivas para las pruebas Spring Boot cuenta con la anotación @TestConfiguration.

     
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    class CountryServiceTest {
    
  3. Será habitual que necesitemos parámetros de configuración específicos para las pruebas como, por ejemplo, la conexión a la base de datos. Podemos aplicar las siguientes estrategias:
    1. Duplicar en el directorio /src/test/resources los ficheros de /src/main/resources y modificarlos con los parámetros para los tests. Esto incluye el fichero de configuración de Spring boot application.properties (o application.yml).
    2. Crear ficheros de propiedades nuevos que sobrescriban solo los parámetros que cambian para los tests, e indicar estos ficheros en la clase con los tests con la anotación @TestPropertySource.
    3. Modificar valores concretos con el atributo properties de @SpringBootTest.
      @SpringBootTest(properties = "spring.datasource.url=jdbc:mysql://localhost:3306/country-test")
      

    Sigamos la segunda estrategia para establecer la url de la base de datos de pruebas. Creamos el fichero /src/test/resources/db-test.properties con el siguiente contenido:

    spring.datasource.url=jdbc:mysql://localhost:3306/country-test
    

    y lo declaramos en la clase

    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    @TestPropertySource(locations = "classpath:db-test.properties")
    class CountryServiceTest {
    
  4. Si al hacer testing usamos una base de datos, necesitamos establecer un juego de datos predeterminado en el que basar las pruebas. Incluso es muy probable que tests distintos requieran de juegos de datos diferentes. La práctica habitual en el mundo Spring consiste en crear el script SQL correspondiente o los que hagan falta dentro del directorio /src/test/resources/ y declararlos en la anotación @Sql. Si anotamos la clase, los scripts se ejecutarán antes de cada prueba (esto se puede cambiar con la propiedad executionPhase). También es posible anotar un método concreto, en cuyo caso no se aplicará a esa prueba la anotación @Sql que pudiera tener la clase.
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    @TestPropertySource(locations = "classpath:db-test.properties")
    @Sql("/test-mysql.sql")
    class CountryServiceTest {
    
BEGIN;
TRUNCATE countries;

INSERT INTO `countries` (`id`, `create_by`, `created_date`, `last_modified_by`, `last_modified_date`, `name`, `population`) VALUES
    (1, 'test', '2018-11-17 18:50:06', 'test', '2018-11-17 18:50:10', 'Mexico', 130497248),
    (2, 'test', '2018-11-17 18:50:06', 'test', '2018-11-17 18:50:10', 'Spain', 49067981),
    (3, 'test', '2018-11-17 18:50:06', 'test', '2018-11-17 18:50:10', 'Colombia', 46070146);

COMMIT;

Ahora estamos en condiciones de inyectar en CountryServiceTest los beans de Spring que necesitemos. Comprobamos que la llamada a CountryService#findAll retorna los tres países existentes en la tabla.

package com.danielme.springboot.services;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
@Sql("/test-mysql.sql")
class CountryServiceTest {

    @Autowired
    CountryService countryService;

    @Test
    void testFindAll() {
        assertEquals(3, countryService.findAll().size());
    }

}

Con Jupiter, se permiten inyecciones como argumentos del método. JUnit 4 no admite métodos de test con parámetros.

@Test
void testFindAll(@Autowired CountryService countryService) {

Antes de ejecutar la prueba, hay que asegurar que los parámetros de conexión a la base de datos MySQL están bien configurados (db.properties), y que existe la base de datos y el usuario con los permisos adecuados. Para mayor comodidad, he incluido en la raíz del proyecto un fichero Dockerfile basado en la imagen oficial de MySQL. Al crearse un contenedor, se configura todo lo necesario porque se ejecutará el script init.sql.

Con estas órdenes se construye la imagen y arranca un contenedor de usar y tirar (la opción –rm borrará el contenedor cuando se detenga).

docker build -t spring-boot-demo-mysql .
docker run --rm -d -p 3306:3306 spring-boot-demo-mysql

Docker 1: introducción
Docker 2: imágenes y contenedores

Obsérvese que el contenedor expone el puerto 3306 de MySQL hacia fuera a través del puerto 3306. Si tenemos más de un MySQL en ejecución, cada uno deberá usar un puerto distinto. Si estás en esta situación, usa uno distinto al crear el contenedor y tenlo en cuenta al configurar los datos de conexión.

Nota. La ejecución de contenedores para la realización de pruebas se puede integrar en nuestros tests de Junit 5 con la librería TestContainers

¡Ha llegado el momento de la verdad! Ejecutemos la prueba con Maven o nuestro IDE favorito tal y como explico aquí. El resultado debe ser «verde».

El starter de Spring Boot para testing incluye la librería AssertJ. Nunca me canso de recomendarla porque proporciona una API fácil de usar e increíblemente potente para la construcción de aserciones. Hablo un poco sobre ella aquí.

assertThat(countries)
                .extracting(Country::getName)
                .containsExactlyInAnyOrder(
                        Dataset.NAME_COLOMBIA,
                        Dataset.NAME_MEXICO,
                        Dataset.NAME_SPAIN);

Es fácil apreciar que la nueva aserción resulta más precisa, ya que comprueba que la lista contenga los tres países existentes indicados por su nombre (el atributo name de Country definido por la referencia a su método get). Una aserción muy fácil de escribir gracias a AssertJ.

Base de datos en memoria

Para ejecutar con éxito el test anterior no basta con tener acceso a un servidor MySQL en el que podamos realizar las pruebas. Además, durante la ejecución de las mismas debe ser de uso exclusivo y así evitar incongruencias es los datos esperados.

Lo anterior implica que para ejecutar las pruebas no es suficiente contar con las herramientas de construcción del proyecto (en nuestro caso, Maven 3.5 y Java 11), sino que es necesario instalar en el equipo una infraestructura de software más compleja (el servidor MySQL). Hoy en día esto no supone un gran inconveniente porque con Docker podemos tener una imagen para los tests con la versión de la base de datos que utiliza la aplicación en producción, estrategia que ya se comentó líneas atrás. Y la gestión de los contenedores se puede delegar en Testcontainers. Debemos aspirar a que las pruebas se puedan ejecutar en cualquier equipo, como suele decirse, con solo pulsar botón.

Una opción para conseguir esto último en lo que respecta a las pruebas que acceden a bases de datos consiste en emplear una base de datos en memoria. Spring Boot ofrece dos estrategias.

  • Configurar un datasource específico para los tests. Por ejemplo, para H2 la configuración estándar es la siguiente
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
    spring.datasource.username=sa
    spring.datasource.password=sa
    
  • Dejar que Spring Boot lo haga automáticamente marcando la clase con @AutoConfigureTestDatabase. Se reemplazará el datasource de la aplicación con uno generado para la base de datos embebida que se encuentre en el classpath (H2, HSQLDB o Derby).

En ambos casos hay que añadir la base de datos al pom, por ejemplo H2.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

Ahora creamos un nuevo script de inicialización de SQL válido para H2 teniendo en cuenta además que los cambios realizados en esta BD no serán guardados.

INSERT INTO countries (id, create_by, created_date, last_modified_by, last_modified_date, name, population) VALUES
    (1, 'test', '2018-11-17 18:50:06', 'test', '2018-11-17 18:50:10', 'Mexico', 130497248),
    (2, 'test', '2018-11-17 18:50:06', 'test', '2018-11-17 18:50:10', 'Spain', 49067981);

La siguiente clase es una copia de CountryServiceTest que utiliza H2. Se ha sobrescrito el dialecto para que Hibernate pueda crear automáticamente la tabla countries.

@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {"spring.jpa.database-platform=org.hibernate.dialect.H2Dialect"})
@Sql("/test-h2.sql")
@AutoConfigureTestDatabase
class CountryServiceH2Test {

    @Autowired
    CountryService countryService;

    @Test
    void testFindAll() {
        List<Country> countries = countryService.findAll();

        assertThat(countries)
                .extracting(Country::getName)
                .containsExactlyInAnyOrder(
                        Dataset.NAME_MEXICO,
                        Dataset.NAME_SPAIN);
    }

}

Revisando la bitácora vemos que, efectivamente, se configuró el datasource para H2.

11-17-2018 09:35:53,846 PM  INFO TestDatabaseAutoConfiguration$EmbeddedDataSourceBeanFactoryPostProcessor:106 - Replacing 'dataSource' DataSource bean with embedded version
11-17-2018 09:35:53,847 PM  INFO DefaultListableBeanFactory:821 - Overriding bean definition for bean 'dataSource' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari; factoryMethodName=dataSource; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]] with [Root bean: class [org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration$EmbeddedDataSourceFactoryBean]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
11-17-2018 09:35:54,341 PM  INFO PostProcessorRegistrationDelegate$BeanPostProcessorChecker:326 - Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$56f34ab] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
11-17-2018 09:35:54,429 PM  INFO EmbeddedDatabaseFactory:189 - Starting embedded database: url='jdbc:h2:mem:63084dbe-31c8-40aa-99c7-3e6e13518c95;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'

Personalmente no me gusta esta estrategia. Si no empleamos la misma base de datos que utiliza la aplicación en una instalación real, la validez de las pruebas es un tanto cuestionable. Asimismo, no suele resultar viable cuando utilizamos SQL, pues en algunos casos éste será incompatible con las capacidades de la BD en memoria.

Integración con Mockito

Mockito es una librería fantástica para la creación de objetos de tipo dobles de tests (test doubles). Puesto que ya publiqué el tutorial Test Doubles con Mockito, me voy a centrar en la integración con Spring Boot.

Spring Boot ofrece una integración excepcional con Mockito gracias a las anotaciones @MockBean y @SpyBean. Ambas crean mocks y spys que reemplazan dentro del contexto de Spring a sus beans equivalentes. Veámoslo con un ejemplo.

Creamos la clase CountryServiceMockAnnotationTest análoga a CountryServiceTest en la que vamos a testear CountryService (la instancia de tipo singleton gestionada por el contendor de beans de Spring). El servicio tiene como dependencia CountryRepository, y vamos a declarar un mock del mismo con @MockBean. Lo más destacable del ejemplo es el hecho de que la integración de Spring Boot con Mockito ya se encarga de que en el bean CountryService se inyecte el mock de forma automática y transparente. He usado verify para comprobar que se ha invocado al método findAll del mock.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
public class CountryServiceMockAnnotationTest {

    @MockBean
    CountryRepository countryRepository;

    @Autowired
    CountryService countryService;

    @Test
    public void testFindAllEmptyResponse() {
        when(countryRepository.findAll()).thenReturn(Collections.emptyList());

        assertThat(countryService.findAll()).isEmpty();
        verify(countryRepository, times(1)).findAll();
    }

}

@SpyMock puede causar problemas con repositorios de Spring Data JPA. Esto fue resuelto en Spring Boot 2.5.3 de acuerdo a esta incidencia. Tengamos presente que los beans de Spring son, en realidad, objetos proxy, lo que puede darnos algún que otro inconveniente cuando trabajamos con Mockito.

Probando controladores web\API REST

MockMVC

En Spring es posible probar los controladores simulando la realización de llamadas HTTP gracias a la clase MockMVC. La idea es testear una api REST de forma «interna» sin tener que desplegar la aplicación en un servidor, pero también de forma realista efectuando las llamadas a la misma tal y como lo harían los consumidores del servicio.

Si bien la clase MockMVC no es exclusiva de Spring Boot, con él su uso se simplifica. Es capaz de crear y configurar una instancia de MockMVC si usamos la anotación @AutoConfigureMockMvc. Así inyectamos una instancia de MockMvc lista para ser utilizada.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
@Sql("/test-mysql.sql")
@AutoConfigureMockMvc
public class CountryRestControllerTest {

    @Autowired
    private MockMvc mockMvc;

}

MockMVC ofrece una «api fluida» que permite hacer una llamada web con todos los parámetros que sean necesarios, así como validar la corrección de las respuestas. Escribamos algunas pruebas para los servicios REST de CountryRestController. Son los siguientes.

Empecemos probando la obtención de un país. Lo primero es realizar la llamada a una url con el método perform. Proporcionaremos su configuración, incluyendo su contenido si fuera necesario (por ejemplo el envío con POST de un JSON), en la forma de un objeto de tipo RequestBuilder. Su construcción es fácil con los métodos estáticos de MockMvcRequestBuilders.

ResultActions result = mockMvc.perform(
                          get(CountryRestController.COUNTRIES_RESOURCE + "/{id}/", SPAIN_ID)
                       );

Al objeto result iremos concatenando con el método andExpect las verificaciones que queremos aplicar a la respuesta y que funcionarán como aserciones. Lo haremos construyendo objetos de tipo ResultMatcher con MockMvcResultMatchers.

result
       .andExpect(status().is(HttpStatus.OK.value()))
       .andExpect(jsonPath("$.name", is(Dataset.NAME_SPAIN)));

En el ejemplo se usa la librería JsonPath, incluida en el starter de test de Spring Boot, para realizar validaciones sobre los JSON de respuesta aplicando expresiones de Hamcrest. Estas expresiones no suelen fáciles de construir, así que quizás nos resulte más práctico obtener todo el JSON devuelto y compararlo con la cadena esperada.

String response = result.andReturn()
                .getResponse()
                .getContentAsString();

Así queda la prueba.

    @Test
    void testGetSpain() throws Exception {
        String response = mockMvc.perform(get(CountryRestController.COUNTRIES_RESOURCE + "/{id}/", SPAIN_ID))
                .andExpect(status().is(HttpStatus.OK.value()))
                .andExpect(jsonPath("$.name", is(NAME_SPAIN)))
                .andReturn()
                .getResponse()
                .getContentAsString();

        logger.info("response: " + response);
    }

A la hora de probar la creación de una nueva entidad, necesitamos enviar una petición de tipo POST que contenga en su cuerpo o body los datos en formato JSON. Podemos construir un objeto CountryRequest y convertirlo en JSON si inyectamos en CountryRestControllerTest una instancia de ObjectMapper. Esta clase pertenece a la librería Jackson en la que Spring se apoya para realizar las conversiones de objetos en JSON y viceversa.

Siguiendo las anteriores indicaciones, estos dos métodos comprueban la creación correcta de un nuevo país y que se devuelva un error 400 cuando el nuevo país no tenga, erróneamente, un nombre.

@Autowired
private MockMvc mockMvc;

@Autowired
ObjectMapper objectmapper;

@Test  
void testAddGermany() throws Exception {
    CountryRequest country = new CountryRequest("Germany", 79778000);

    String response = mockMvc
            .perform(post(CountryRestController.COUNTRIES_RESOURCE)
                    .content(objectmapper.writeValueAsString(country))
                    .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().is(HttpStatus.CREATED.value()))
            .andReturn()
            .getResponse()
            .getContentAsString();

    logger.info(response);
}

@Test
void testNoNameCreateCountry() throws Exception {
    CountryRequest country = new CountryRequest(null, 1);

    String response = mockMvc
            .perform(post(CountryRestController.COUNTRIES_RESOURCE)
                    .content(objectmapper.writeValueAsString(country))
                    .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().is(HttpStatus.BAD_REQUEST.value()))
            .andReturn()
            .getResponse()
            .getContentAsString();

    logger.info(response);
}
WebTestClient

Aunque MockMvc «hace el trabajo», merece la pena destacar que también contamos con WebTestClient, una API muy similar. Fue creada para probar las APIs reactivas introducidas en Spring 5 con el módulo WebFlux. Desde Spring Boot 2.6 (noviembre 2021) es compatible con Spring MVC. La idea es contar con una única API que permita probar indistintamente ambas tecnologías.

A continuación dejo una versión de testGetSpain basada en WebTestClient.

@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
@Sql("/test-mysql.sql")
@AutoConfigureWebTestClient
class CountryRestControllerTest {

    @Test
    void testGetSpainWebTestClient(@Autowired WebTestClient client) {
        byte[] response = client.get().uri(CountryRestController.COUNTRIES_RESOURCE + "/{id}", SPAIN_ID)
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus()
                .isOk()
                .expectBody()
                .jsonPath("name").isEqualTo(NAME_SPAIN)
                .returnResult()
                .getResponseBody();

        logger.info(new String(response, StandardCharsets.UTF_8) );
    }

Si se produjera este error.

 No qualifying bean of type 'org.springframework.test.web.reactive.server.WebTestClient' available

Hay que añadir el starter para WebFlux.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <scope>test</scope>
</dependency>

REST Assured

Rest Assured es una poderosa librería Java para el testeo de apis REST muy popular. Podemos utilizar su API sobre MockMVC, ya que existe una implementación que proporciona la integración. Para utilizarla, tenemos que incluir en el pom la dependencia io.rest-assured\spring-mock-mvc. La versión adecuada, como es habitual, la establece Spring Boot.

<dependency>
  <groupId>io.rest-assured</groupId>
  <artifactId>spring-mock-mvc</artifactId>
  <scope>test</scope>
</dependency>

Las pruebas, en lugar de basarse en la clase io.restassured.RestAssured, deberán utilizar io.restassured.module.mockmvc.RestAssuredMockMvc para poder beneficiarnos de la integración con MockMVC.

Veamos unos ejemplos. Este método es una implementación alternativa a testGetSpain basada en REST Assured. Obsérvese que hay que proporcionar a RestAssuredMockMvc la instancia de MockMVC a través de la cual interactuará con la API REST.

@Test
void testGetSpainRestAssured() {
    RestAssuredMockMvc.mockMvc(mockMvc);

    String response = when().get(CountryRestController.COUNTRIES_RESOURCE + "/{id}/", SPAIN_ID)
            .then()
            .statusCode(HttpStatus.OK.value())
            .body("name", equalTo(NAME_SPAIN))
            .extract()
            .asString();

    logger.info(response);
}

Tiene algo menos de código y es más fácil validar un campo del JSON. En esta otra prueba, RestAssured es capaz de transformar el resultado en una lista de países.

@Test
void testGetAllRestAssured() throws JsonProcessingException {
    RestAssuredMockMvc.mockMvc(mockMvc);

    List<Country> countries = when().get(CountryRestController.COUNTRIES_RESOURCE)
            .then()
            .statusCode(HttpStatus.OK.value())
            .extract()
            .as(new TypeRef<>() {});

    assertThat(countries)
            .extracting(Country::getName)
            .containsExactlyInAnyOrder(
                    NAME_COLOMBIA,
                    NAME_MEXICO,
                    NAME_SPAIN);

    logger.info(objectmapper.writeValueAsString(countries));
}

Estas son las pruebas para los restantes servicios REST (actualización y borrado) con RestAssured.

    @Test
    void testUpdateRestAssured() {
        RestAssuredMockMvc.mockMvc(mockMvc);
        String newName = "new name";
        Integer newPopulation = 1000;
        CountryRequest countryRequest = new CountryRequest(newName, newPopulation);

        given()
                .header("Content-type", "application/json")
                .and()
                .body(countryRequest)
                .when()
                .put(CountryRestController.COUNTRIES_RESOURCE + "/" + SPAIN_ID)
                .then()
                .statusCode(HttpStatus.NO_CONTENT.value());

        Optional<Country> country = countryRepository.findById((long) SPAIN_ID);
        assertThat(country).isNotEmpty();
        assertThat(country.get().getName()).isEqualTo(newName);
        assertThat(country.get().getPopulation()).isEqualTo(newPopulation);
    }

    @Test
    void testDeleteRestAssured() {
        RestAssuredMockMvc.mockMvc(mockMvc);

        given()
                .when()
                .delete(CountryRestController.COUNTRIES_RESOURCE + "/" + SPAIN_ID)
                .then()
                .statusCode(HttpStatus.NO_CONTENT.value());

        Optional<Country> country = countryRepository.findById((long) SPAIN_ID);
        assertThat(country).isEmpty();
    }

Clases de configuración para pruebas

Probablemente encontremos casos en los que las anotaciones @MockBean y @SpyBean se nos queden cortas y necesitemos construir con código algunos beans de Spring específicos para ciertos tests. Podemos usar la técnica «tradicional» basada en la combinación de clases de configuración y perfiles que explico al final de este tutorial.

De nuevo, todo resulta más sencillo con Spring Boot porque permite crear clases de configuración para ser aplicadas solo en las pruebas si las marcamos con la anotación @TestConfiguration en lugar de @Configuration. Estas clases pueden escribirse de dos maneras:

  1. En su propio fichero. Se importa en las clases de pruebas en las que se quiera utilizar con @Import.
  2. Si solo se van a utilizar en una clase de pruebas, basta con crearlas como una clase interna estática para que se apliquen automáticamente. Pero si en la anotación @SpringBootTest estamos especificando configuraciones con el atributo classes, será necesario añadir a la declaración esta clase interna de configuración.

Veamos un ejemplo simple. Vamos a crear un ObjectMapper de Jackson que formatee de forma bonita («pretty printing») el JSON para utilizarlo en las pruebas de la clase CountryRestControllerTest en la que se están imprimiendo los JSON de respuesta en la bitácora. Puesto que solo lo queremos cuando se ejecuten las pruebas de esa clase, lo hacemos con una clase interna marcada con @TestConfiguration. En un método «factoría» -anotación @Bean- creamos y devolvemos un objeto ObjectMapper que se usará en lugar del que Spring Boot configura de manera automática.

Importante: esto es un ejemplo, se puede configurar Jackson en el fichero application.properties.

class CountryRestControllerTest {

    private static final Logger logger = LoggerFactory.getLogger(CountryRestControllerTest.class);

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ObjectMapper objectmapper;

    @TestConfiguration
    static class TestConfigurationApp {
        @Bean
        ObjectMapper objectMapperPrettyPrinting() {
            return JsonMapper.builder()
                    .enable(SerializationFeature.INDENT_OUTPUT)
                    .addModule(new JavaTimeModule())
                    .build();
        }
    }

Es posible que al crear un ObjectMapper «a mano» se lance esta excepción.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"

Se solventa añadiendo el módulo JavaTimeModule.

Ahora el JSON devuelto por los servicios REST resulta legible para un humano.

c.d.s.c.CountryRestControllerTest        : response: {
  "createdDate" : 1542480606000,
  "lastModifiedDate" : 1542480610000,
  "createBy" : "test",
  "lastModifiedBy" : "test",
  "id" : 2,
  "name" : "Spain",
  "population" : 49067981
}

Veamos otro ejemplo de clase de configuración para pruebas. Se trata de una versión alternativa de CountryServiceMockAnnotationTest en la que el mock se construye en una factoría (clase de configuración) aplicada con @Import.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
@Import(TestFactory.class)
class CountryServiceMockFactoryTest {

    @Autowired
    CountryRepository countryRepository;

    @Autowired
    CountryService countryService;
@TestConfiguration
class TestFactory {

    @Bean
    @Primary
    CountryRepository countryRepositoryMock() {
        CountryRepository mock = mock(CountryRepository.class);
        when(mock.findAll()).thenReturn(Collections.emptyList());
        return mock;
    }

}

Puesto que estamos creando un nuevo bean para CountryRepository, Spring tiene dos instancias distintas que puede inyectar: la «real» (countryRepository) y la creada por TestConfigurationApp (countryRepositoryMock). Solucionamos esta ambigüedad con la anotación @Primary para que nuestro countryRepositoryMock se inyecte siempre de forma predeterminada. Por consiguiente, cuando se aplique TestConfigurationApp todas las inyecciones de CountryRepository, tanto las solicitadas en las clases de pruebas como en las clases «reales» se resolverán con countryRepositoryMock.

Test slices \ Selección de capas

Hasta ahora, hemos «levantado» para cada clase con pruebas el contexto de Spring al completo, lo que a veces resulta desproporcionado y ralentiza la ejecución. Aunque lo óptimo es que todas las clases utilicen la misma configuración de Spring, como veremos más adelante, es posible configurar en cada clase los componentes de Spring que es necesario instanciar, y esto podemos hacerlo creando múltiples clases de configuración personalizadas.

Por fortuna hay una solución más simple y rápida recurriendo a algunas anotaciones de Spring Boot alternativas a @SpringBootTest. Esta característica se conoce como «Test slices» y permite indicar las capas o secciones de Spring que necesitamos que estén disponibles para las pruebas. Los dos «slices» más importantes son las siguientes.

WebMvcTest

Nos ofrece la capa web, incluyendo controladores, filtros y Spring Security. Queda fuera, por tanto, todos los demás beans de Spring: la capa de persistencia con Spring Data JPA, los servicios (@Service) y los componentes genéricos (@Component), entre otros. Como ejemplo, vamos a crear una nueva versión de CountryRestControllerTest#testGetSpain.

@ExtendWith(SpringExtension.class)
@WebMvcTest(CountryRestController.class)
class CountryRestControllerOnlyWebTest {

    private static final Logger logger = LoggerFactory.getLogger(CountryRestControllerOnlyWebTest.class);

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    CountryService countryService;

    @Test
    void testGetSpain() throws Exception {
        Mockito.when(countryService.findById((long) SPAIN_ID)).thenReturn(Optional.of(new Country("Spain", 0)));

        String response = mockMvc.perform(get(CountryRestController.COUNTRIES_RESOURCE + "/{id}/", SPAIN_ID))
                .andExpect(status().is(HttpStatus.OK.value()))
                .andExpect(jsonPath("$.name", is("Spain")))
                .andReturn().getResponse()
                .getContentAsString();

        logger.info("response: " + response);
    }

}

Hay que definir un mock de CountryService, pues es dependencia de CountryRestController y, debido a @WebMvcTest, Spring no lo crea. Para mejorar aún más el rendimiento, se pueden acotar en @WebMvcTest los controladores que necesitamos.

@DataJpaTest

Configura solo los repositorios y lo que ello implica (el acceso a base de datos).

@ExtendWith(SpringExtension.class)
@TestPropertySource(locations = "classpath:db-test.properties")
@Sql("/test-mysql.sql")
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class CountryRepositoryTest {

    @Autowired
    CountryRepository countryRepository;

    @Test
    void testFindAll() {
        assertThat(countryRepository.findAll()).hasSize(3);
    }

}

@DataJpaTest incluye @AutoConfigureTestDatabase por lo que el test usará una base de datos en memoria. Si queremos utilizar la base de datos MySQL definida para las pruebas en db-test.properties, deshabilitamos esta funcionalidad con el atributo replace de la anotación @AutoConfigureTestDatabase.

Rendimiento

A estas alturas, el lector habrá podido constatar por sí mismo que los tests de integración tiene un coste elevado con respecto al tiempo de ejecución porque es necesario levantar el contexto de Spring. Si bien el framework intenta reutilizar el contexto durante toda la ejecución conjunta de un grupos de clases con pruebas, en algunos (demasiados) casos no es posible porque hay cambios de la configuración utilizada, el perfil y\o las propiedades, o se utilizan las anotaciones @MockBean, @DirtiesContext o @SpyBean.

Esto es fácil de ver con nuestro ejemplo. Si ejecutamos de forma conjunta las clases CountryServiceTest y CountryServiceJUnit4Test, veremos que en el log aparece una única vez el logotipo (configurable) que Spring Boot muestra durante el arranque. Sin embargo, esto cambia si añadimos a la ejecución la clase CountryServiceMockAnnotationTest. Pese a que utiliza, en apariencia, la misma configuración de Spring que las otras dos clases, «mockea» un bean, lo que requiere la creación de un contexto específico. En esta ocasión, si echamos un vistazo al log veremos que Spring se ha levantado dos veces.

En consecuencia, si no prestamos atención, a medida que la aplicación crezca y con ella las pruebas, la ejecución de estas últimas puede eternizarse. Así pues, procuraremos reutilizar la misma configuración para el mayor número de pruebas y minimizar el uso de mocks.

JUnit 4

Además de Jupiter, JUnit 5 cuenta con otra librería llamada «JUnit Vintage» cuyo cometido es la ejecución de pruebas escritas con JUnit 4 y JUnit 3. Nos permitirá dar el salto a JUnit 5 en proyectos que ya cuenten con pruebas para versiones anteriores de JUnit sin tener que modificarlas.

JUnit Vintage dejó de incluirse de forma predeterminada en el starter para tests de Spring Boot 2.4 (noviembre 2020). Sin embargo, esto no es ningún problema, pues podemos traerla de vuelta añadiendo esta dependencia.

        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.hamcrest</groupId>
                    <artifactId>hamcrest-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

El testing con JUnit 4 en Spring ya lo traté en este tutorial, aunque sin Spring Boot. Sirva de ejemplo rápido la siguiente versión de la clase CountryServiceTest.

@RunWith(SpringRunner.class)
@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
@Sql("/test-mysql.sql")
public class CountryServiceJUnit4Test {

    @Autowired
    CountryService countryService;

    @Test
    public void testFindAll() {
        assertThat(countryService.findAll()).hasSize(3);
    }

}

Al contar con Jupiter y Vintage en el mismo proyecto, debemos ser cuidadosos y usar la anotación @Test correspondiente a Vintage. Pero la gran diferencia con respecto a CountryRepositoryTest es la necesidad de declarar el runner que integra Spring con JUnit 4.

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.

Otros tutoriales relacionados

Spring Boot: Gestión de errores en aplicaciones web y REST
Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA
Testing Spring con JUnit 4
Testing con JUnit 4
Introducción a JUnit 5
Persistencia en BD con Spring Data JPA
Spring JDBC Template: simplificando el uso de SQL
Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

Un comentario sobre “Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos en memoria.

  1. Excelente articulo, muy preciso para cada tipo de test… por ahi que a veces no cargan los ccs de la página pero eso no tiene que ver con la calidad del articulo

Deja una respuesta

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. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.