1. Introducción
En este tutorial, vamos a explorar cómo usar testcontainers en una aplicación Spring Boot al crear pruebas de integración.
Los Testcontainers son instancias ligeras y desechables de servicios de infraestructura, como bases de datos, intermediarios de mensajes, etc., que pueden ejecutarse en contenedores Docker. Al escribir pruebas de integración, nos gustaría tener estos servicios disponibles en lugar de simularlos o manejar la configuración y limpieza utilizando scripts personalizados propensos a errores.
Veamos cómo podemos configurar un contenedor de base de datos utilizando testcontainers.
2. Requisitos previos
Primero, necesitamos configurar nuestro proyecto para trabajar con testcontainers. Podemos descargar e instalar Docker desde el sitio web oficial: https://www.docker.com/get-started
A continuación, necesitamos agregar las dependencias de testcontainers en nuestro archivo pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
</dependency>
<dependency
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
Asegúrate de obtener la última versión de las dependencias de testcontainers junit-jupiter y testcontainers-postgresql. Además, asegúrate de utilizar la versión correcta de spring-boot-testcontainers, ya que algunas clases podrían no estar disponibles en la versión anterior de Spring Boot.
3. Uso de Testcontainers en Pruebas de Spring Boot
Spring Boot ha incluido soporte para Testcontainers desde hace un tiempo. Sin embargo, con el lanzamiento de Spring Boot 3.1, la integración se ha mejorado aún más. Veamos cómo se compara el enfoque antiguo con el nuevo.
En versiones anteriores a Spring Boot 3.1, teníamos que configurar manualmente Spring Boot para conectarnos a los servicios que se ejecutan dentro de los contenedores. Esto a menudo conduce a incluir un poco de código repetitivo que implica el uso de la anotación @DynamicPropertySource para establecer las propiedades necesarias:
@SpringBootTest
@Testcontainers
class MyIntegrationTests {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void myTest() {
// ...
}
@DynamicPropertySource
static void postgresProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
Arriba, anotamos la prueba con @Testcontainer para informar a la clase de prueba que utiliza testcontainers. Luego definimos una instancia del contenedor de PostgreSQL usando la anotación @Container. Esto iniciará un contenedor de PostgreSQL con la versión especificada. Normalmente, habríamos configurado las propiedades de conexión dentro del archivo application.properties. Pero como estamos utilizando testcontainers, aún no sabemos cuáles serán las propiedades de conexión. Así que usamos @DynamicPropertySource para recuperar las propiedades de conexión de la instancia del contenedor y establecerlas en el contexto de la aplicación de Spring Boot.
Como podemos ver, este enfoque implica algunas partes móviles y es muy verboso incluso para una prueba simple.
A partir de Spring Boot 3.1, podemos utilizar la anotación @ServiceConnection para simplificar el proceso:
@SpringBootTest
@Testcontainers
class MyIntegrationTests {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void myTest() {
// ...
}
}
Aquí, hemos reemplazado la anotación @DynamicPropertySource con @ServiceConnection. Esto elimina la necesidad de recordar todas las propiedades para cualquier servicio dado y permite que Spring Boot gestione automáticamente la configuración de la conexión. En detalle, funciona descubriendo el tipo de contenedor y creando un bean correspondiente de ConnectionDetails. Luego, Spring Boot utiliza esto para configurar la conexión al servicio que se está ejecutando en el Testcontainer.
Podemos consultar todos los contenedores compatibles en la documentación oficial de Spring Boot.
4. Usando Testcontainers Durante el Desarrollo
Demos un paso más para ver cómo podemos utilizar Testcontainers durante el desarrollo aprovechando las nuevas características de Spring Boot 3.1.
Históricamente, solíamos usar diferentes perfiles para alternar entre los entornos de desarrollo y prueba. Pero ahora, hay una mejor manera de gestionar esto y hacer que el entorno de desarrollo sea lo más similar posible al entorno de producción.
En lugar de crear un perfil de prueba, podemos crear un nuevo método main para ejecutar nuestra aplicación localmente utilizando testcontainers:
public class TestMyApplication {
public static void main(String[] args) {
SpringApplication.from(MyApplication::main).run(args);
}
}
Debemos crear esta clase dentro del paquete raíz de pruebas para mantener las dependencias de testcontainers solo en el ámbito de prueba. De lo contrario, las dependencias se incluyen en archivos jar completos, lo que aumenta el tamaño de la aplicación y no se utiliza en producción.
Ahora, necesitamos configurar el contenedor de prueba para PostgreSQL y hacer que el nuevo método principal lo reconozca. Primero, creamos un archivo de configuración y lo anotamos con @TestConfiguration:
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16");
}
}
En resumen, solo estamos creando un bean que representa la instancia del contenedor de PostgreSQL. Nuevamente, lo anotamos con @ServiceConnection para informar a Spring Boot que debe gestionar el ciclo de vida y configurarlo para usarlo con nuestra aplicación.
Ahora, actualizamos el método test-main:
public class TestMyApplication {
public static void main(String[] args) {
SpringApplication.from(MyApplication::main)
.with(MyContainersConfiguration.class)
.run(args);
}
}
Finalmente, podemos ejecutar el método test-main para iniciar la aplicación utilizando Maven:
./mvnw spring-boot:test-run
O utilizando Gradle:
./gradlew bootTestRun
Esto iniciará la aplicación y los contenedores se iniciarán y se apagarán automáticamente junto con la aplicación. Como podemos ver, los plugins de maven y gradle ya conocen el método test-main y comenzarán la aplicación utilizando eso.
5. Guardando los Datos del Contenedor Entre Reinicios
Con la configuración anterior, estamos creando una nueva instancia del contenedor cada vez que se inicia la aplicación. Por lo tanto, perdemos todos sus datos. Sin embargo, podríamos querer guardar el estado del contenedor entre reinicios. Para hacer esto, podemos utilizar la anotación @RestartScope de Spring Boot DevTools en los métodos del bean del contenedor:
@Bean
@ServiceConnection
@RestartScope
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16");
}
Alternativamente, podemos utilizar la función experimental de contenedores reutilizables en Testcontainers llamando a withReuse(true) en el contenedor:
new PostgreSQLContainer<>("postgres:16").withReuse(true);
Esto guardará los datos entre reinicios. Además, esto acelera el tiempo de inicio de la aplicación, ya que no creamos un nuevo contenedor cada vez.
6. Conclusión
En este artículo, hemos visto cómo utilizar Testcontainers en pruebas de integración de Spring Boot. Hemos aprendido a configurar un contenedor de base de datos utilizando Testcontainers y cómo usar la anotación @ServiceConnection para simplificar el proceso. También hemos explorado cómo usar Testcontainers durante el desarrollo con las nuevas características de Spring Boot 3.1.