Testing Spring con JUnit 4

Última actualización: 01/11/2020

logo spring

En el tutorial Testing con JUnit 4 vimos la importancia del testing automatizado y los primeros pasos para realizarlo en Java de la mano de JUnit 4. En los proyectos en los que utilicemos Spring necesitaremos acceder en algunas clases de tests a los beans proporcionados por el contenedor de dependencias y, en muchos de ellos, utilizar versiones mocks, stubs, dummys, etc de los mismos. De este modo podemos realizar test funcionales y de integración de forma efectiva y realista.

Spring proporciona soporte para testing, utilizable tanto en JUnit como TestNG, a través del módulo spring-test que además de permitir la inyección de beans en los tests ofrece numerosas utilidades para trabajar con transacciones, lanzar scripts de sql, llamar a controladores de Spring MVC, etc.

En este pequeño tutorial no seremos tan ambiciosos y me limitaré a los conocimientos mínimos necesarios para empezar a escribir pruebas automáticas en aplicaciones basadas en Spring con JUnit 4. En cualquier caso, recomiendo el uso de Spring Boot porque facilita enormemente la configuración de Spring, incluyendo el desarrollo de las pruebas.

Nota: Spring 5 proporciona soporte oficial para JUnit5 con SpringExtension.

Proyecto para pruebas

Vamos a testear una aplicación muy sencilla basada en Spring 5 que consta de un bean(Dependency) que a su vez depende de otro bean(SubDependency). Todo lo que veamos es trasladable a Spring 4, pues se trata de configuraciones básicas disponibles desde hace muchos años en este marco de trabajo.

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;

	@Value("${url}")
	private String url;

	public Dependency(SubDependency subDependency) {
		super();
		this.subDependency = subDependency;
	}

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

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

	public int addTwo(int i) {
		return i + 2;
	}	

	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 hace a través del constructor y que en este caso no es necesario utilizar @Autowired puesto que al haber un único constructor, Spring se verá forzado a utilizarlo para poder instanciar la clase. Realizar la inyección de dependencias mediante constructor es una buena práctica por muchas razones. Una de ellas es que en los tests si hiciera falta podemos construir cualquier dependencia «manualmente» sin utilizar Spring y evitándose tener que crear setters o utilizar reflection para acceder a los atributos en los que hacer la inyección.

La configuración de Spring está definida con JavaConfig. Importamos el fichero /src/main/resources/values.properties con la propiedad «url» a inyectar en Dependency.url. También 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 {

}

Por último, en el pom.xml además de la dependencia de Spring (spring-context), tenemos las dependencias necesarias para utilizar log4j2 integrado con Spring.

<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</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.2.1.RELEASE</springframework.version>
        <mavencompiler.version>3.2</mavencompiler.version>
        <log4jframework.version>2.5</log4jframework.version>
        <junit.version>4.13.1</junit.version>
        <slf4j.version>1.7.19</slf4j.version>
    </properties>

    <dependencies>

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

        <!-- Loggin 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>

        <!-- Testing -->

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

    </dependencies>

    <build>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${mavencompiler.version}</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Configurar el test con JUnit

Vamos a crear un test de JUnit que pruebe la clase Dependency con la instancia creada e inyectada por Spring que será la que utilice nuestra aplicación «real» tal cual. Se trata de un ejemplo sencillo con el que será fácil -espero- comprender los conceptos.

Seguimos los siguientes pasos:

  1. Añadir las dependencias del módulo spring-test y de JUnit 4.
    
             <junit.version>4.13.1</junit.version>
    	</properties>
             ...
    	<!-- Testing -->
    
    		<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. Declarar el runner SpringJUnit4ClassRunner.
    @RunWith(SpringJUnit4ClassRunner.class)
    
  3. Importar solo la configuración de Spring que se usará en este test con la anotación @ContextConfiguration. Indicaremos las clases o los XML de configuración, ejemplos:
    @ContextConfiguration(classes = {AppConfiguration.class})
    @ContextConfiguration("file:src/main/resources/applicationContext.xml")
    @ContextConfiguration(locations = { "file:src/test/resources/applicationContext.xml" })
    
  4. Si fuera necesario utilizar versiones específicas para testing de los valores contenidos en los ficheros properties de /src/main/resources que estamos leyendo en Spring, por ejemplo los parámetros de configuración de la base de datos de prueba, tenemos que «sobrescribir» esos valores en ficheros properties en /src/test/resources y solicitar a Spring que lea estas propiedades con la anotación @TestPropertySource.

    Para probar esta casuística he creado el fichero /src/test/resources/test.properties. Lo importamos en el test del siguiente modo.

    @TestPropertySource("classpath:test.properties")
    
  5. Por último, realizamos la inyección en atributos de los beans que queramos.
    	@Autowired
    	private Dependency dependency;
    
  6. Con respecto a los logs, en el ejemplo estoy utilizando una configuración de log4j2 específica para los test con el fichero /src/test/resources/log4j2.xml que se usará en lugar de /src/main/resources/log4j2.xml

La clase con nuestros tests queda finalmente así.

package com.danielme.spring.test;
import static org.junit.Assert.*;

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

import com.danielme.spring.test.Dependency;
import com.danielme.spring.test.SubDependency;

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

	@Autowired
	private Dependency dependency;
	
	@Test
	public void testDependency(){		
		assertEquals(dependency.getClass().getSimpleName(), dependency.getClassName());		
	}
	
	@Test
	public void testAddTwo(){		
		assertEquals(3, dependency.addTwo(1));		
	}
	
	@Test
	public void testSubdependency(){		
		assertEquals(SubDependency.class.getSimpleName(), dependency.getSubdependencyClassName());
	}
	
	@Test
	public void testUrl(){		
		assertEquals("http://danielmedina.info", dependency.getUrl());
	}
}

El hecho de integrar Spring con JUnit mediante SpringJUnit4ClassRunner puede suponer un problema: solo podemos usar un runner por clase, por lo que hay que renunciar a utilizar otros, como por ejemplo el JUnitParamsRunner que vimos en el tutorial de JUnit.

Por fortuna, desde Spring 4.2 hay una forma alternativa de hacer la integración mediante reglas del siguiente modo:

@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();

En ambos casos los tests se ejecutan correctamente.

Mocks y beans personalizados para testing

Aunque estamos realizando test de integración, en más de una ocasión no será raro que tengamos que modificar o incluso «mockear» el comportamiento de algunos beans. Un ejemplo típico consiste en simular los servicios externos que consume nuestra aplicación, por ejemplo una Api REST. Otro caso habitual puede ser simulación de errores para verificar que la aplicación los procesa de forma adecuada.

Si utilizamos Spring Boot, contamos con integración directa con Mockito gracias a las anotaciones @MockBean y @SpyBean. Lo explico en este tutorial: Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos embebidas. Pero en nuestro ejemplo, tendremos que construir manualmente los mocks como beans de Spring y conseguir que esos beans reemplacen a los «reales» empleados en la aplicación.

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;
    }

}

La clase anterior de configuración (@Configuration) tiene un método factoría (@Bean) que crea un bean de tipo SubDependency llamado subDependencyMocked (nombre del método) de tipo Singleton (el ámbito por omisión de todos los beans). Hasta aquí, nada que no hagamos de forma habitual cuando necesitamos definir beans de forma programática (esto es, construyendo el objeto mediante código). Lo peculiar del ejemplo es lo siguiente.

  • Se ha usado la anotación @Primary para que se subDependencyMocked sea la instancia de SubDependency que Spring inyecte de forma predeterminada donde sea requerida. Tengamos en cuenta que ahora hay dos alternativas a la hora de inyectar SubDependency y debemos informar a Spring cuál escoger.
  • Las configuraciones definidas en MockConfiguration se usarán en todos las pruebas. Si no queremos este comportamiento, creamos un perfil que permita decidir en cada clase con pruebas las configuraciones a aplicar. Por ello, he creado el perfil mockConfiguration con @Profile.

La siguiente clase activa el perfil, así que el bean para Dependency tendrá la versión mock de SubDependency. Lo validamos con el test correspondiente.

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, la prueba testSubdependency sigue funcionando porque Dependency contiene la versión «no mock» de SubDependency.

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

Tal y como cabría esperar, la estrategia basada en combinar perfiles y clases de configuración para reemplazar en los tests beans por otros construidos de forma programática es válida si trabajamos con Spring Boot. No obstante, será más cómodo recurrir a la anotación @TestConfiguration. De nuevo, remito al lector a este tutorial.

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 sobre testing

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

Testing con JUnit 4

Test doubles con Mockito

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.