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

Última actualización: 28/10/2020

logo springEn el tutorial Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA se construye una pequeña aplicación web que muestra en pantalla los registros de una tabla de una base de datos MySQL a la que se accede a través de Spring Data JPA, y que también dispone de una pequeña api REST.

Sin embargo, falta un elemento vital como son los tests. Para suplir esta carencia, en el presente tutorial voy a mostrar cómo configurar tests de integración en Spring Boot utilizando esa misma aplicación así como algunas opciones y librerías que pueden ser de gran ayuda. Estos tests necesitarán para su ejecución levantar el contexto de Spring, y su objetivo no es tanto validar el funcionamiento de la aplicación sino “jugar” con las funcionalidades más básicas que Spring Boot y esas librerias proporcionan.

Los tests se desarrollarán con JUnit 4 pero al final del tutorial veremos cómo utilizar también JUnit 5. Esta versión del tutorial está actualizada a Spring Boot 2.3.3 (agosto 2020), y se indicarán algunas diferencias significativas con respecto a versiones anteriores para ayudar al lector en el caso de que trabaje con aplicaciones más antiguas.

El siguiente diagrama de clases UML proporciona una vista general de la estructura de la aplicación a testear.

Escribiendo el primer test

Vamos a añadir un test de JUnit 4 que haga uso del bean CountryService.

  1. Necesitamos el starter spring-boot-starter-test que ya incluye JUnit 4 de serie. Aunque a partir de Spring Boot 2.2 (octubre 2019) JUnit 5 es la versión por defecto, se continúa incluyendo soporte para JUnit 4 “out-of-the-box”, esto es, sin necesidad de realizar ninguna configuración adicional.
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
  2. Creamos la clase que contendrá los tests y la marcamos, de momento, con dos anotaciones:
    • Indicamos que queremos ejecutar los test con SpringRunner o SpringJUnit4ClassRunner porque necesitamos levantar el contexto de Spring para utilizar sus beans.
    • Con @SpringBootTest levantamos el contexto de Spring al completo. Por defecto se utiliza de forma automática la configuración de una clase anotada con @SpringBootApplication. Spring busca desde el paquete en el que se encuentre la clase de test y hacia arriba -siguiendo la jerarquía de paquetes- una clase con @SpringBootApplication, tomando la primera que encuentre. Si tenemos varias y no queremos que se aplique este comportamiento por defecto, se puede especificar la que queremos utilizar con el atributo classes de @SpringBootTest. En este atributo también podemos proporcionar otras clases de configuración (@Configuration) adicionales aunque, como veremos más adelante, para crear configuraciones específicas para tests Spring Boot cuenta con la anotación @TestConfiguration.

     
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class CountryServiceTest {
    
  3. Para los tests habitualmente tendremos que utilizar parámetros de configuración específicos como por ejemplo la conexión a la base de datos. Podemos aplicar las siguientes estrategias:
    • 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).
    • Crear ficheros de propiedades completamente nuevos que sobrescriban sólo los parámetros que cambian para los tests, e indicar estos ficheros en la clase con los tests con la anotación @TestPropertySource.
    • Modificar valores de forma específica sólo para una clase de test con el atributo properties o values de @SpringBootTest.
      @SpringBootTest(properties = "spring.datasource.url=jdbc:mysql://localhost:3306/country-test")
      

    Por ejemplo, vamos a seguir la segunda estrategia para establecer la url de la base de datos en MySQL que usaremos específicamente para los tests. Se crea el fichero /src/test/resources/db-test.properties con el siguiente contenido:

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

    y lo añadimos en el test

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @TestPropertySource(locations = "classpath:db-test.properties")
    public class CountryServiceTest {
    
  4. Puesto que estamos utilizando una base de datos en nuestro test, generalmente vamos a querer precargar un juego de datos pues los test han de ser deterministas y funcionar siempre igual, ofreciendo los mismos resultados para las mismas entradas. Para ello creamos el script sql correspondiente dentro del directorio /src/test/resources/ y lo importamos en el test con la anotación @Sql para que se ejecute antes del test.
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @TestPropertySource(locations = "classpath:db-test.properties")
    @Sql("/test-mysql.sql")
    public 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;

Cursos de programación

Ahora estamos en condiciones de inyectar en el test los beans de Spring que necesitemos, y nuestro primer test queda así.

package com.danielme.springboot.services;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
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.junit4.SpringRunner;

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

    @Autowired
    CountryService countryService;

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

}

Pero antes de ejecutarlo hay que comprobar que los parámetros de conexión a la base de datos MySQL están correctamente configurados, y que en el servidor de MySQL existe la base de datos y el usuario con los permisos adecuados. Para mayor comodidad, he incluido un Dockerfile en la raíz del proyecto. La imagen se puede construir y arrancar como un contenedor utilizando, por ejemplo, los siguientes comandos.

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

El starter de Spring Boot para testing incluye la librería AssertJ ya mencionada en el tutorial Testing con JUnit 4 y que permite escribir aserciones con una api fluida. La utilizaré en el resto del tutorial por ser más legible y potente que los assert propios de JUnit.

assertThat(countryService.findAll()).hasSize(3);

Base de datos embebida en memoria

Para ejecutar exitosamente el test anterior es necesario tener acceso a un servidor MySQL en el que podamos realizar las pruebas y que durante su ejecución sea de uso exclusivo para así evitar incongruencias con los datos que espera el test.

Dicho de otro modo, para ejecutar los tests no basta con tener las herramientas para construir y ejecutar el proyecto (en nuestro caso, Maven 3 y Java 8), sino que es necesario instalar en el equipo una infraestructura de software más compleja. Hoy en día esto no supone una gran dificultad ya que con Docker podemos instanciar un contenedor específicamente 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. Incluso podemos integrar directamente contenedores con JUnit mediante la librería TestContainers.

Una alternativa ampliamente utilizada para poder ejecutar los tests que acceden a bases de datos de forma “autónoma” y sin ningún requisito externo al propio código consiste en emplear una base de datos embebida en memoria. Con Spring Boot podemos hacerlo fácilmente siguiendo una de las siguientes 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 el test 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);

Vamos a crear una copia de CountryServiceTest que utilice H2. Obsérvese que se ha tenido que sobrescribir el dialecto de Hibernate.

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

    @Autowired
    CountryService countryService;

    @Test
    public void test() {
        assertThat(countryService.findAll()).hasSize(2);
    }

}

Revisando el log de Spring podemos comprobar que, efectivamente, se configura 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 pues si no usamos exactamente la misma base de datos que utiliza la aplicación en una instalación final la validez de los test es muy cuestionable. A esto que hay que añadir los problemas que pueden producirse si utilizamos SQL nativo pues en muchos casos será incompatible con la BD embebida.

Master Pyhton, Java, Scala or Ruby

Integración con Mockito

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

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

Creamos la clase CountryMockServiceTest 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 utiliza el repositorio CountryRepository, y vamos a hacer un mock del mismo. 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 totalmente automática y transparente para nosotros. Y, como cualquier otro mock, tenemos disponible la funcionalidad verify.

package com.danielme.springboot.services;

import com.danielme.springboot.repositories.CountryRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@TestPropertySource(locations = "classpath:db-test.properties")
public class CountryMockServiceTest {

    @MockBean
    CountryRepository countryRepository;

    @Autowired
    CountryService countryService;

    @Test
    public void test() {
        when(countryRepository.findAll()).thenReturn(new ArrayList<>());

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

}

IMPORTANTE: @SpyMock no funciona con repositorios de Spring Data JPA, consultar la issue para alternativas.

Controladores web\API REST

En Spring es posible testear directamente los controladores simulando la realización de llamadas HTTP a través de la clase MockMVC. Esto permite probar una api REST de forma “interna”, sin tener que desplegar la aplicación en un servidor, pero de forma realista realizando las llamadas a la misma tal y como lo haría una aplicación cliente, y con la posibilidad de usar mocks dentro del test.

La clase MockMVC no es exclusiva de Spring Boot ya que forma parte de Spring desde la versión 3.2, pero Spring Boot es capaz de crear y configurar automáticamente su instancia simplemente utilizando la anotación @AutoConfigureMockMvc. De este modo podemos inyectar cómodamente en la clase de test una instancia de MockMvc lista para ser utilizada.

package com.danielme.springboot.controllers;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
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.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.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 y obtener y comprobar la respuesta. No voy a analizar su uso, pero a modo de ejemplo mostraré algunos tests para el endpoint REST definido en la clase CountryRestController.

Básicamente lo que hay que hacer es realizar la llamada a una url mediante el método perform al que proporcionaremos la configuración de la llamada HTTP incluyendo su contenido si fuera necesario (por ejemplo el envío con POST de un JSON). A la respuesta de la llamada a perform iremos concatenando las restricciones que queremos imponer a la respuesta (andExpect) y que funcionarán a modo de assert del test.

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. Para realizar la serialización y deserialización de objetos Java en JSON, he inyectado ObjectMapper, el mapeador de Jackson que es configurado automáticamente por Spring.

package com.danielme.springboot.controllers;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import com.danielme.springboot.entities.Country;
import com.fasterxml.jackson.databind.ObjectMapper;

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

    private static final Logger logger = LoggerFactory.getLogger(CountryRestControllerTest.class);
    
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ObjectMapper objectmapper;

    @Test
    public void testGetSpain() throws Exception {
        String response = mockMvc.perform(get(apiRootPath +"/country/{id}/", 2))
                .andExpect(status().is(HttpStatus.OK.value()))
                .andExpect(jsonPath("$.name", is("Spain")))
                .andReturn().getResponse().getContentAsString();

        logger.info("response: " + response);
    }
    
    @Test
    public void testAddGermany() throws Exception {
        Country country = new Country("Germany", 79778000);
        
        String response = mockMvc.perform(post(apiRootPath + "/country/")
                .content(objectmapper.writeValueAsString(country))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().is(HttpStatus.CREATED.value()))
                .andReturn().getResponse().getContentAsString();
        
        logger.info(response);
    }
    
    @Test
    public void testAddDuplicateCountry() throws Exception {
        Country country = new Country("Spain", 1);

        String response = mockMvc.perform(post(apiRootPath + "/country/")
                .content(objectmapper.writeValueAsString(country))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()))
                .andReturn().getResponse().getContentAsString();

        logger.info(response);
    }

}

Clases de configuración para tests

Además de poder utilizar la potencia de Mockito, a veces nos será también muy útil configurar beans de Spring de forma específica para ciertos tests. Podemos crear clases de configuración de Spring para ser aplicadas sólo en tests si las marcamos con la anotación @TestConfiguration. Esta configuración puede hacerse de dos formas:

  1. Crearla en su propia clase e importarla en las clases de tests en las que se quiera utilizar con la anotación @Import.
  2. Si sólo se van a utilizar en una clase de tests, basta con crearlas como una clase interna estática de la misma para que se apliquen de forma automática. Pero si en la anotación @SpringBootTest estamos especificando configuraciones con el atributo classes, será necesario añadir también esta clase interna de configuración.

Veamos un ejemplo muy sencillo. Vamos a crear un ObjectMapper de Jackson que formatee de forma bonita (“pretty printing”) el JSON para utilizarla en los tests de la clase CountryRestControllerTest en la que se están imprimiendo los JSON de respuesta en el log. Puesto que sólo lo vamos a utilizar en este test, lo hacemos mediante una clase interna. Definimos un ObjectMapper que se usará en lugar del que Spring Boot genera de forma automática.

Nota: esto es sólo un ejemplo. Se puede configurar el mapeador de Jackson directamente en el application.properties.


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

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

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ObjectMapper objectmapper;

    @TestConfiguration
    static class TestConfigurationApp {
        @Bean
        ObjectMapper objectMapperPrettyPrinting() {
            return new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
        }
    }

Ahora el json que devuelve el servicio REST y que se imprime en el log de los tests está formateado.

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

La estrategia anterior permite definir mocks sin necesidad de utilizar las anotaciones @MockBean y @SpyBean las cuales, recordemos, sólo tenemos disponibles en aplicaciones de Spring Boot. Incluso utilizando Spring Boot, esta estrategia nos permitiría realizar configuraciones de mocks justo en el momento de su creación. Por ejemplo

    @TestConfiguration
    static class TestConfigurationApp {
        @Bean
        @Primary
        CountryRepository countryRepositoryMock() {
            CountryRepository mock = mock(CountryRepository.class);
            when(mock.findAll()).thenReturn(new ArrayList<>());
            return mock;
        }
    }

Puesto que estamos creando un nuevo bean (countryRepositoryMock), ahora Spring tiene dos instancias distintas de CountryRepository para inyectar (countryRepository y countryRepositoryMock). Usamos la anotación @Primary para que nuestro mock se inyecte siempre por defecto.

Segmentación de capas

Hasta ahora, hemos levantado para cada clase con tests el contexto de Spring al completo, lo que a veces resulta desproporcionado y ralentiza la ejecución. Aunque lo óptimo es que todos las clases de test utilicen la misma configuración, como veremos más adelante cuando hablemos de rendimiento, es posible configurar en cada test los componentes de Spring que es necesario instanciar, y esto podemos hacerlo creando múltiples clases de configuración personalizadas. Pero hay una solución más simple y rápida recurriendo a algunas anotaciones de Spring Boot para reemplazar a @SpringBootTest. Veamos un par de ellas.

  • WebMvcTest. Levanta la capa web, incluyendo controladores, filtros y Spring Security. Queda fuera, por tanto, toda la capa de persistencia con Spring Data JPA y los servicios. Como ejemplo, vamos a crear un una versión del test CountryRestControllerTest#testGetSpain
    package com.danielme.springboot.controllers;
    
    import com.danielme.springboot.entities.Country;
    import com.danielme.springboot.services.CountryService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mockito;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.http.HttpStatus;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    
    import java.util.Optional;
    
    import static org.hamcrest.Matchers.is;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    @RunWith(SpringRunner.class)
    @WebMvcTest(CountryRestController.class)
    public class CountryRestControllerOnlyWebTest {
    
        private static final Logger logger = LoggerFactory.getLogger(CountryRestControllerOnlyWebTest.class);
    
        private static final long SPAIN_ID = 2;
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        CountryService countryService;
    
        @Test
        public void testGetSpain() throws Exception {
            Mockito.when(countryService.findById(SPAIN_ID)).thenReturn(Optional.of(new Country("Spain", 0)));
            String response = mockMvc.perform(get(CountryRestController.COUNTRY_RESOURCE + "/{id}/", SPAIN_ID))
                    .andExpect(status().is(HttpStatus.OK.value()))
                    .andExpect(jsonPath("$.name", is("Spain"))).andReturn().getResponse()
                    .getContentAsString();
    
            logger.info("response: " + response);
        }
    
    }
    
    

    Nótese que hay que hacer un mock de CountryService pues es dependencia de CountryRestController y ahora Spring ya no crea ese bean por pertenecer a la capa de servicios (@Service).

  • @DataJpaTest. Levanta sólo los repositorios y su datasource. Ideal para testear las consultas a base de datos. Por ejemplo:
    package com.danielme.springboot.repository;
    
    import com.danielme.springboot.repositories.CountryRepository;
    import com.danielme.springboot.repositories.CustomAuditorAware;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
    import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.test.context.TestPropertySource;
    import org.springframework.test.context.jdbc.Sql;
    import org.springframework.test.context.junit4.SpringRunner;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.springframework.context.annotation.FilterType.ASSIGNABLE_TYPE;
    
    @RunWith(SpringRunner.class)
    @TestPropertySource(locations = "classpath:db-test.properties")
    @Sql("/test-mysql.sql")
    @DataJpaTest(includeFilters = @ComponentScan.Filter(
            type = ASSIGNABLE_TYPE,
            classes = {CustomAuditorAware.class}
    ))
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    public class CountryRepositoryTest {
    
        @Autowired
        CountryRepository countryRepository;
    
        @Test
        public void updateTest() {
            assertThat(countryRepository.findAll()).hasSize(3);
        }
    
    }
    
    

    A tener en cuenta:

    • @DataJpaTest incluye @AutoConfigureTestDatabase por lo que el test usará una base de datos embebida en memoria. Si queremos utilizar la base de datos MySQL definida para los tests, incluimos la anotación @AutoConfigureTestDatabase para deshabilitar esta funcionalidad.
    • La clase CustomAuditorAware para la auditoria de entidades es un @Component y no se instancia. Forzamos su creación aplicando la solución propuesta en este enlace.

    Rendimiento

    Implementar tests de integración tiene un coste elevado en lo que respecta al tiempo de ejecución ya que es necesario levantar el contexto de Spring. El framework intenta reutilizar el contexto durante toda la ejecución conjunta de un grupos de tests pero 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 CountryServiceJUnit5Test, veremos que en el log sólo aparece una vez el logotipo (configurable) que Spring Boot muestra durante el arranque. Sin embargo, esto cambia si también añadimos a la ejecución la clase CountryMockServiceTest, porque pese a utilizar 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.

    Por tanto, si no prestamos atención, a medida que la aplicación crezca y con ella los tests, estos últimos pueden eternizarse no tanto por la ejecución de los propios tests sino por la configuración de su infraestructura. En la medida de lo posible, hay que utilizar la misma configuración común para el mayor número de tests y minimizar el uso de mocks.

    REST Assured

    Rest Assured es una librería Java para el testeo de apis REST ampliamente utilizada. Si estamos acostumbrados a su uso podemos utilizar su API sobre MockMVC ya que existe una implementación que proporciona esta integración.

    Para utilizarla, tenemos que incluir en el pom la dependencia io.rest-assured\spring-mock-mvc. La versión ya es establecida por Spring Boot.

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

    Los tests, 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.

    A continuación se muestra una implementación alternativa a testGetSpain basada en REST Assured. Obsérvese que hay que proporcionar a RestAssuredMockMvc la instancia de MockMVC que debe utilizar para realizar las llamadas a la API REST, generalmente esto lo haremos en el método @Before de la clase JUnit si tenemos que hacerlo para todos los tests que contenga la clase.

        @Test
        public void testRestAssured() {
            RestAssuredMockMvc.mockMvc(mockMvc);
            
            when().get(apiRootPath + "/country/{id}/", 2)
            .then().statusCode(HttpStatus.OK.value())
                    .body("name", equalTo("Spain"));
        }
    

    JUnit 5

    Spring Boot test proporciona JUnit 4 pero podemos utilizar JUnit 5 para escribir los tests (o incluso utilizar ambas versiones a la vez). A partir de Spring Boot 2.2, y tal y como indiqué anteriormente, el soporte de JUnit 5 está incluido de serie, pero si todavía estamos utilizando versiones anteriores tendremos que configurarlo del siguiente modo:

    • Sólo JUnit 5: añadimos las siguientes dependencias
      <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-api</artifactId>
          <scope>test</scope>
      </dependency>
      
      <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-engine</artifactId>
          <scope>test</scope>
      </dependency>
      

      excluimos JUnit 4 para evitar confundirnos con las clases

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
          <exclusions>
              <exclusion>
              <groupId>junit</groupId>
              <artifactId>junit</artifactId>
          </exclusion>
          </exclusions>
      </dependency>
      

      Nos aseguramos de utilizar como mínimo la versión 2.22.0 del plugin surefire de Maven cuya responsabilidad es ejecutar los tests. La versión de este plugin ya es configurada por Spring Boot pero en versiones un tanto antiguas (por ejemplo Spring Boot 2.0.6) se utiliza la 2.21.0.

      <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.0</version>
          <configuration>
              <useSystemClassLoader>false</useSystemClassLoader>
          </configuration>
      </plugin>
      
    • JUnit 4 y JUnit 5 a la vez. Además de los cambios anteriores, excepto la exclusión de JUnit 4, añadimos la siguiente librería que proporciona soporte para la ejecución de tests implementados con JUnit 3 y JUnit 4 utilizando el nuevo engine de JUnit 5.
      <dependency>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
          <scope>test</scope>
      </dependency>
      

    En ambos casos, si utilizamos una versión muy antigua de Eclipse es posible que los tests no puedan ejecutarse y se produzca el siguiente error

    java.lang.NoClassDefFoundError: org/junit/platform/launcher/core/LauncherFactory
        at org.eclipse.jdt.internal.junit5.runner.JUnit5TestLoader.(JUnit5TestLoader.java:31)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at java.lang.Class.newInstance(Class.java:442)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.createRawTestLoader(RemoteTestRunner.java:367)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.createLoader(RemoteTestRunner.java:362)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.defaultInit(RemoteTestRunner.java:306)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.init(RemoteTestRunner.java:221)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:205)
    Caused by: java.lang.ClassNotFoundException: org.junit.platform.launcher.core.LauncherFactory
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 11 more
    

    Se soluciona añadiendo la siguiente librería al pom.

    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-launcher</artifactId>
        <version>1.1.0</version>
        <scope>test</scope>
    </dependency>
    

    A modo de ejemplo, la siguiente clase implementa con JUnit 5 el test CountryServiceTest. Aquí ya no usamos un runner para integrar el test con Spring sino el nuevo y potente mecanismo de extensiones de JUnit 5 con la extensión SpringExtension.

    package com.danielme.springboot.services;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    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;
    
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    @TestPropertySource(locations = "classpath:db-test.properties")
    @Sql("/test-mysql.sql")
    public class CountryServiceJUnit5Test {
    
        @Autowired
        CountryService countryService;
    
        @Test
        public void test() {
            assertEquals(3, countryService.findAll().size());
            // assertj
            assertThat(countryService.findAll()).hasSize(3);
        }
    
    }
    

    En JUnit 5 podemos utilizar las anotaciones de Spring @EnabledIf y @DisabledIf que habilitan y deshabilitan respectivamente la ejecución de los tests evaluando una expresión SpEL. Estas anotaciones pueden aplicarse tanto a un método de test como a una clase completa.

    Por ejemplo, el siguiente test se ejecuta en función del valor de la propiedad spring.datasource.username definida en el application.properties; en este caso concreto es necesario activar el flag loadContext porque la expresión requiere utilizar el contexto de Spring.

    @EnabledIf(expression = "#{'${spring.datasource.username}' != 'demo' }", loadContext = true)
    @Test
    public void testIf1() {
        fail("fail");
    }
    

    El siguiente test está copiado directamente de la documentación y sólo se ejecuta en plataformas Linux.

    @EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('linux')}")
    @Test
    public void testIf2() {
        fail("fail");
    }
    

    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
    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 embebidas.

    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

    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 )

    Google photo

    Estás comentando usando tu cuenta de Google. 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 )

    Conectando a %s

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