Testing Spring con JUnit 4

Última actualización: 21/12/2022
logo spring

En los proyectos desarrollados con Spring Framework necesitaremos acceder a los beans del contenedor de dependencias para crear pruebas de integración, end-to-end y similares. En ocasiones, estas pruebas requerirán dobles de test (mocks) de algunos beans con el fin de testear casos específicos o bien simular sistemas externos cuyo empleo en las pruebas resulta complicado, cuando no imposible.

Spring proporciona un magnífico soporte para testing a través del módulo spring-test con el denominado «Spring TestContext Framework». Además de permitir la inyección de beans en las pruebas, este framework provee numerosas utilidades para trabajar con transacciones, lanzar scripts de SQL o realizar peticiones HTTP a controladores de Spring MVC, entre muchas otras capacidades.

En este pequeño tutorial no seremos tan ambiciosos. Me limitaré a presentar los conocimientos mínimos necesarios para empezar a desarrollar pruebas automáticas con JUnit 4 y Spring. Descubrirás que se precisa muy poco.

Si tienes escasa o nula experiencia con JUnit 4, te sugiero consultar este tutorial.

  1. Notas sobre Spring Boot y JUnit 5
  2. Proyecto de ejemplo
  3. Configurar el test con JUnit 4
    1. La clase de prueba paso a paso
    2. Los tests
    3. Usar reglas en vez del runner
  4. Reemplazar dependencias con dobles de tests. Mockito.
  5. Cómo ejecutar las pruebas de JUnit 4 en JUnit 5
  6. Código de ejemplo

Notas sobre Spring Boot y JUnit 5

Cuando publiqué la primera versión del tutorial allá por 2017, Spring Boot todavía era algo novedoso, aunque ya tenía una importante tracción en el mercado, y aún no se había publicado JUnit 5.

En el momento de la última revisión (diciembre de 2022), huelga decir que tanto Spring Boot como JUnit 5 son el estándar en sus ámbitos. Esta circunstancia otorga al presente tutorial un valor fundamentalmente histórico: puede ser de ayuda a quienes todavía deban lidiar con proyectos basados en versiones de Spring muy antiguas.

Si no te encuentras en esta situación y quieres dar tus primeros pasos en el testing con Spring, te recomiendo este completo tutorial:

Con todo, ten en cuenta que en JUnit 5 puedes ejecutar pruebas escritas con JUnit 3 y 4 sin cambiar su código ni los mecanismos de integración con Spring que veremos en breve. Te mostraré cómo hacerlo al final del tutorial.

Proyecto de ejemplo

Usaremos un proyecto Maven Java 8 basado en Spring 5. Lo tienes en GitHub. No obstante, todo lo que veremos son funcionalidades disponibles en Spring desde hace bastantes años.

Este es el pom inicial. Cuenta con spring-context, el corazón de Spring, y las dependencias para establecer Apache Log4j 2 como proveedor del sistema de logs. Lo puedes configurar en el fichero /src/main/resources/log4j2.xml si sientes la curiosidad de saber qué ocurre cuando ejecutes las pruebas:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.danielme.spring</groupId>
    <artifactId>spring-test</artifactId>
    <version>1.0</version>
    <name>spring-testing-junit4</name>
    <packaging>jar</packaging>

    <description>Spring testing</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <springframework.version>5.3.24</springframework.version>
        <log4jframework.version>2.19.0</log4jframework.version>        
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${springframework.version}</version>
        </dependency>

        <!-- Log with Log4j2 - Spring integration with SL4J Facade -->

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4jframework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-jcl</artifactId>
            <version>${log4jframework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4jframework.version}</version>
        </dependency>        

    </dependencies>

</project>

Vamos a testear una aplicación sencilla que consta de un bean llamado Dependency. A su vez depende de otro llamado SubDependency.

package com.danielme.spring.test;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Dependency {

    private final SubDependency subDependency;
    private final String url;

    public Dependency(SubDependency subDependency, @Value("${url}") String url) {
        this.subDependency = subDependency;
        this.url = url;
    }

    public String getSubdependencyClassName() {
        return subDependency.getClassName();
    }

    public String getUrl() {
        return url;
    }

}
package com.danielme.spring.test;

import org.springframework.stereotype.Component;

@Component
public class SubDependency {

	public String getClassName() {
		return this.getClass().getSimpleName();
	}
}

Obsérvese que la inyección de dependencias en Dependency se efectúa vía constructor. No hizo falta @Autowired: al existir un único constructor con parámetros, Spring se verá forzado a usarlo para crear la clase.

Realizar la inyección de dependencias mediante constructor es una buena práctica por muchas razones. La que más nos interesa es que en las pruebas podemos construir cualquier dependencia a mano, y esto evita la necesidad del contexto de Spring. De este modo se facilita el desarrollo de pruebas unitarias.

El atributo Dependency#url contiene el valor de url establecido en el fichero de propiedades /src/main/resources/values.properties.

url = https://danielme.com

La configuración de Spring está definida con JavaConfig en la siguiente clase (anotación @Configuration). En ella importamos el fichero values.properties con @PropertySource y activamos el escaneo automático de anotaciones con @ComponentScan:

package com.danielme.spring.test;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@ComponentScan
@PropertySource("classpath:values.properties")
public class AppConfiguration {

}

Configurar el test con JUnit 4

Vamos a escribir una prueba de JUnit para Dependency. Usaremos su instancia creada por Spring, la que se inyectará cuando lo solicitemos con @Autowired.

La clase de prueba paso a paso

Sigamos esta receta:

1-.Añadir las dependencias spring-test y JUnit 4 con el ámbito test, pues solo se requieren en el código con las pruebas.

 <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${springframework.version}</version>
        <scope>test</scope>
 </dependency>

 <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
 </dependency>

2-. Creamos la clase que contendrá las pruebas (clase de prueba) en una carpeta dentro de /src/test/java respetando la estructura de directorios dictada por Maven. Asimismo, el nombre de la clase debe cumplir con estos patrones; la constumbre habitual es añadirles el sufijo «Test».

package com.danielme.spring.test;

public class DependencyTest {
}

3-. Declarar el runner SpringJUnit4ClassRunner. Su cometido es integrar Spring con JUnit 4, de tal forma que cuando ejecutemos las pruebas se arrancará un contexto de Spring (ApplicationContext) apropiado.

@RunWith(SpringJUnit4ClassRunner.class)

4-. Importar las clases de configuración.

@ContextConfiguration(classes = {AppConfiguration.class})

5-.Será probable que necesitemos versiones específicas de los valores contenidos en los ficheros de propiedades que estamos leyendo en Spring. Lo más sencillo es sobrescribir esos valores en ficheros situados en /src/test/resources. Solicitaremos a Spring que los use marcando la clase de prueba con @TestPropertySource.

Para probar esta casuística he creado el fichero /src/test/resources/test.properties. Contiene la misma propiedad que values.properties con un valor distinto:

url = https://danielmedina.info

Lo importamos en la clase de prueba:

@TestPropertySource("classpath:test.properties")

Así queda la clase:

package com.danielme.spring.test;

import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfiguration.class})
@TestPropertySource("classpath:test.properties")
public class DependencyTest {
	
}
Los tests

Ya podemos implementar la prueba que planteé al inicio de esta sección. En DependencyTest inyectamos Dependency para chequear en métodos públicos anotados con @Test que las operaciones de Dependency hacen lo que tienen que hacer, ni más ni menos.

	@Autowired
	private Dependency dependency;

	@Test
	public void testSubdependency(){
		assertEquals(SubDependency.class.getSimpleName(), dependency.getSubdependencyClassName());
	}

	@Test
	public void testUrl(){
		assertEquals("https://danielmedina.info", dependency.getUrl());
	}

Puedes ejecutar las pruebas con cualquier entorno de desarrollo, como Eclipse o IntelliJ. La última es la más relevante. Fíjate que comprueba que el valor Dependency#url es el definido en el fichero test.properties en vez del que está en el fichero values.properties. Si quitas la anotación @TestPropertySource, testUrl fallará porque Dependency#url contendrá la url declarada en values.properties.

Recuerda que en el fichero /src/test/resources/log4j2.xml puedes configurar las trazas del log para ver con detalle qué hacen Spring y JUnit.

Usar reglas en vez del runner

Integrar Spring y JUnit con SpringJUnit4ClassRunner puede suponer un problema: solo podemos usar un runner por clase. Nos obliga a renunciar a todos los demás.

Por fortuna, desde Spring 4.2 la integración es posible con el empleo de reglas:

@ContextConfiguration(classes = {AppConfiguration.class})
@TestPropertySource("classpath:test.properties")
public class DependencyTest {

	@ClassRule
	public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();
	@Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

Reemplazar dependencias con dobles de tests. Mockito.

Aunque estemos realizando pruebas de integración, es probable que en más de una ocasión tengamos que simular el comportamiento de algunos beans. Un ejemplo típico consiste en emular los servicios externos que consume nuestra aplicación, como una API REST. Otro caso habitual puede ser el lanzamiento de excepciones para verificar que la aplicación los procesa de manera adecuada. O evitar las llamadas a base de datos de un objeto DAO.

La solución consiste en crear «dobles de tests» que simulen lo que queramos. La librería Mockito, con sus objetos mock y spy, lo pone fácil. Tienes una introducción en este tutorial:

El desafío será conseguir que los dobles sean beans de Spring que sustituyan a los beans «reales».

Si usas Spring Boot, esto lo tienes resuelto gracias a las anotaciones @MockBean y @SpyBean. En nuestro proyecto tendremos que hacer un poco de artesanía. No te preocupes porque resulta sencillo. Además, la técnica que voy a explicar también te será útil cuando uses Spring Boot y no te sirvan @MockBean y @SpyBean.

Imagina que necesitas que SubDependency#getClassName retorne la cadena «mocked». Lo que haremos es crear una clase de configuración para devolver en un método factoría (@Bean) un objeto de SubDependency que sea un mock de Mockito. Ese mock ya tendrá el nuevo comportamiento deseado para getClassName.

@Bean
SubDependency subDependencyMocked() {
   SubDependency mock = mock(SubDependency.class);
   when(mock.getClassName()).thenReturn("mocked");
   return mock;
}

Hasta aquí, nada que no hagamos de forma habitual cuando definimos beans de manera programática. Pero en el caso de las pruebas hay «truco». Veamos primero el código completo:

package com.danielme.spring.test;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@Profile(MockConfiguration.MOCK_PROFILE)
@Configuration
class MockConfiguration {

    static final String MOCK_PROFILE = "mockConfiguration";

    @Bean
    @Primary
    SubDependency subDependencyMocked() {
        SubDependency mock = mock(SubDependency.class);
        when(mock.getClassName()).thenReturn("mocked");
        return mock;
    }

}

MockConfiguration tiene dos peculiaridades:

  • Usa la anotación @Primary para que subDependencyMocked sea la instancia de SubDependency que Spring inyecte de forma predeterminada. Date cuenta de que cuando se aplica MockConfiguration tenemos dos beans de SubDependency: el que esta clase construye como un mock y el «real» que Spring crea automáticamente por estar la clase SubDependency marcada con un estereotipo (@Component).
  • Las configuraciones definidas en MockConfiguration se aplicarán en todas las pruebas. Si no queremos este comportamiento, crearemos un perfil de Spring que permita decidir en cada clase de prueba las configuraciones a aplicar. Por ello, he creado el perfil mockConfiguration con @Profile. Así, MockConfiguration solo se utilizará cuando su perfil sea uno de los activos.

La siguiente clase activa el perfil anterior con @ActiveProfiles. Por tanto, el bean para Dependency tendrá la versión mock de SubDependency. Lo validamos con la siguiente prueba:

package com.danielme.spring.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.assertEquals;

@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles(MockConfiguration.MOCK_PROFILE)
@ContextConfiguration(classes = {AppConfiguration.class})
public class DependencyMockedTest {

    @Autowired
    private Dependency dependency;

    @Test
    public void testSubdependency() {
        assertEquals("mocked", dependency.getSubdependencyClassName());
    }

}

Dado que en DependencyTest no se está aplicando el perfil, su prueba testSubdependency sigue funcionando porque Dependency contiene la versión «no mock» de SubDependency.

Cómo ejecutar las pruebas de JUnit 4 en JUnit 5

Cierro el tutorial respondiendo a la pregunta que da título a esta sección. Lo primero que debes saber es que JUnit 5 no es una mera actualización, sino una reescritura desde cero que proporciona una plataforma sobre la que desarrollar librerías de testeo. De serie incluye dos: Jupiter, pensada para escribir pruebas mejores que las que hacemos con JUnit 4 gracias a sus novedosas capacidades, y Vintage, cuyo cometido es ejecutar tests de JUnit 3 y 4.

Por consiguiente, podemos actualizar a JUnit 5, escribir las nuevas pruebas con Jupiter y conservar las antiguas sin tener que migrarlas si no lo deseamos. Un plan perfecto 👌

Para actualizar el proyecto de ejemplo a JUnit 5 basta con reemplazar la dependencia de JUnit 4 por la de Vintage:

<!--<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
</dependency>-->

<dependency>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
      <version>5.9.1</version>
      <scope>test</scope>
</dependency>

¡Genial! Más fácil, imposible. Todo lo que hemos visto en este tutorial continúa funcionando del mismo modo.

Sin embargo, este cambio tiene poco sentido: las pruebas de JUnit 4 son las de siempre. Así que querrás añadir Jupiter para sacar provecho de sus grandes ventajas:

<dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.9.1</version>
     <scope>test</scope>
</dependency>

Hace años que los entornos de desarrollo son compatibles con Jupiter. Pero en Maven se precisa la versión 2.22 o superior del plugin Surefire, responsable de la ejecución de las pruebas:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M7</version>
        </plugin>
    </plugins>
</build>

Quiero subrayar que en Jupiter encontrarás algunas anotaciones que comparten el nombre con anotaciones de JUnit 4. Es el caso de @Test:

org.junit.jupiter.api.Test
org.junit.Test

Ten cuidado y elige las anotaciones adecuadas en función del tipo de pruebas que contenga la clase.

Todo está muy bien, pero ¿cómo se programa con JUnit 5 y Spring? Es el tema de estos tutoriales que ya he enlazado:

Spoiler: integrarás Jupiter y Spring aplicando a la clase de prueba una extensión (@ExtendWith(SpringExtension.class) ) en vez del runner o la regla que vimos.

Código de ejemplo

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

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 )

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.