Utilizando docker-compose para pruebas de integración en Spring Boot

0
1417

La versión 3.1 Spring Boot ha sido liberada el pasado mes de mayo (2023) y entre las novedades que incluye se encuentra el soporte nativo para docker-compose, tanto en código productivo como de tests. En este tutorial vamos a hacer uso de docker-compose para configurar los contenedores de nuestros tests de integración evitando el uso de librerías de terceros o configuraciones en plugins de Maven.

0. Índice de contenidos.

  1. Introducción.
  2. Alternativas.
  3. Creación proyecto de prueba.
  4. Creación test de integración.
  5. Uso en proyectos donde se levantan varios contenedores.
  6. Conclusiones.

1. Introducción

Los tests de integración son pruebas automatizadas que se enfocan en verificar el correcto funcionamiento de la interacción entre diferentes componentes de un sistema. A diferencia de las pruebas unitarias, que se centran en probar individualmente las unidades de código, las pruebas de integración se encargan de validar la integración y colaboración adecuada de los diversos módulos, servicios y capas de una aplicación.

Una aplicación típicamente interactúa con diferentes componentes externos, como bases de datos, servicios web, colas de mensajes, servidores de correo, entre otros. Estos servicios externos son esenciales para el funcionamiento correcto de la aplicación, pero pueden presentar desafíos en el entorno de pruebas.

Al utilizar contenedores para levantar estos servicios externos, se crea un entorno controlado y aislado para las pruebas de integración. Esto permite reproducir el entorno de producción de manera más fiel y garantizar que las pruebas se realicen con los mismos servicios y configuraciones que se encontrarían en un entorno real. Además, los contenedores proporcionan una forma conveniente de gestionar y configurar estos servicios externos de manera programática y automatizada

2. Alternativas

Las alternativas más utilizadas para levantar contenedores que estén disponibles en los tests de integración son dos:

  • Haciendo uso de plugins de Maven (Spotify o Fabric8) para configurar y arrancar los contenedores necesarios en la fase pre-integration-test.
  • Haciendo uso de la librería TestContainers totalmente integrada con Junit y su ciclo de vida. En este tutorial explicamos cómo utilizarlo.

Ambas alternativas son totalmente válidas, sin embargo, el uso de docker-compose de forma nativa en nuestros proyectos Spring Boot nos permite conseguir el mismo resultado de forma más sencilla, y utilizando un mecanismo de configuración standar como es docker-compose.

3. Creación proyecto de prueba

Crearemos un pequeño proyecto de prueba basado en la versión 3.1.0 SpringBoot. Nos vamos a la web de Spring Initializr y generamos un proyecto Java con Maven utilizando SpringBoot 3.1.0 y Java 17:
Creación proyecto Spring Boot

Creamos una entidad de dominio muy sencilla y un repositorio SpringDataJpa encargado de gestionarla en base de datos.

@Entity
@SequenceGenerator(name = "sequence_generator", sequenceName = "book_seq", allocationSize = 1)
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence_generator")
    private Long id;

    private String title;

    private String isbn;

    private Integer year;

    private String author;

    //getters and setters
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {

}

A continuación modificamos el pom.xml del proyecto para dar soporte para docker-compose, SpringDataJpa, PostgreSql (será la base de datos que utilicemos para los tests) y Flyway (queremos que la base datos ya esté precargada con ciertos datos).
Para ello añadimos las siguientes dependencias:


<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
	<scope>runtime</scope>
</dependency>

<dependency>
	<groupId>org.flywaydb</groupId>
	<artifactId>flyway-core</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-docker-compose</artifactId>
</dependency>

4. Creación test de integración

Vamos a crear un test de integración muy sencillo para asegurarnos que el repositorio accede correctamente a la base de datos.

Utilizaremos Flyway para gestionar los scripts de base de datos:

  • En /src/main/resources/db/migration disponemos del script V01_01__create_book_table.sql para crear la tabla book:
    CREATE SEQUENCE book_seq AS BIGINT START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
    CREATE TABLE book
    (
    id BIGINT PRIMARY KEY,
    title VARCHAR(200),
    isbn VARCHAR(200),
    year INTEGER,
    author VARCHAR(200)
    );
    
  • En /src/test/resources/db/migration disponemos del script V01_02__insert_books.sql para crear un registro en la tabla book de cara a los tests de integración:
    INSERT INTO book(id, title, isbn, year, author)
    VALUES(nextval('book_seq'), 'Book 1','987-6-54-321', 2023, 'Author 1');
    

Implementamos un test que realiza un count para obtener el número inicial de libros en base de datos, posteriormente inserta un libro y realiza un nuevo count y finalmente elimina el libro creado y realiza un último count.

@DataJpaTest
class BookRepositoryTest {
    @Autowired
    private BookRepository sut;

    @Test
    void crud_book_ok() {
        assertEquals(1, sut.count());

        final Book book = new Book();
        book.setIsbn("123456789");
        book.setAuthor("Author 2");
        book.setTitle("Title 2");
        book.setYear(2023);
        sut.save(book);

        assertEquals(2, sut.count());

        sut.delete(book);

        assertEquals(1, sut.count());
    }
}

Para ejecutar el test correctamente necesitamos tener levantada la base de datos. En este punto entra en acción docker-compose, vamos a arrancar un contenedor Docker con una imagen de PostgreSQL como base de datos con unos parámetros de configuración básicos.
El contenido del fichero yml es el siguiente:

version: '3.8'
services:
  db:
    image: postgres:15.2-alpine3.17
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - '5432:5432'

Este fichero lo almacenamos en /src/test/resources/docker-compose/docker-compose-postgresl.yml

SpringBoot tiene una serie de comportamientos por defecto que debemos de cambiar mediante configuración en el fichero application.properties:

  • Realiza la búsqueda del fichero docker-compose.yml en la raíz del proyecto, si queremos utilizar otro fichero de configuración lo debemos indicar haciendo uso de la propiedad spring.docker.compose.file
  • No realiza búsqueda en las carpetas de test, la propiedad que controla este mecanismo es spring.docker.compose.skip.in-tests

Por tanto, nuestro application.properties queda con el siguiente contenido, hemos añadido la configuración del datasource para apuntar a la imagen Docker de PostgreSQL que hemos definido en el docker-compose:

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.password=postgres
spring.test.database.replace=none

spring.docker.compose.skip.in-tests=false
spring.docker.compose.file=./docker-compose/docker-compose-postgresl.yml

Ahora si podemos ejecutar el test de forma satisfactoria

15:43:14.653 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.autentia.dockercomposetutorial.BookRepositoryTest]: BookRepositoryTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
15:43:14.873 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.autentia.dockercomposetutorial.DockerComposeTutorialApplication for test class com.autentia.dockercomposetutorial.BookRepositoryTest
  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _    
( ( )___ | '_ | '_| | '_ / _` |    
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |___, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.0)

2023-06-08T15:43:15.487+02:00  INFO 2830 --- [           main] c.a.d.BookRepositoryTest                 : Starting BookRepositoryTest using Java 17 with PID 2830 (started by autentia in /Users/autentia/IdeaProjects/tutos/docker-compose-tutorial)
2023-06-08T15:43:15.488+02:00  INFO 2830 --- [           main] c.a.d.BookRepositoryTest                 : No active profile set, falling back to 1 default profile: "default"
2023-06-08T15:43:15.541+02:00  INFO 2830 --- [           main] .s.b.d.c.l.DockerComposeLifecycleManager : Using Docker Compose file '/Users/autentia/IdeaProjects/tutos/docker-compose-tutorial/target/test-classes/docker-compose/docker-compose-postgresl.yml'
2023-06-08T15:43:16.325+02:00  INFO 2830 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container docker-compose-db-1  Created
2023-06-08T15:43:16.328+02:00  INFO 2830 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container docker-compose-db-1  Starting
2023-06-08T15:43:16.625+02:00  INFO 2830 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container docker-compose-db-1  Started
2023-06-08T15:43:16.630+02:00  INFO 2830 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container docker-compose-db-1  Waiting
2023-06-08T15:43:17.165+02:00  INFO 2830 --- [utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container docker-compose-db-1  Healthy
2023-06-08T15:43:18.264+02:00  INFO 2830 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-06-08T15:43:18.326+02:00  INFO 2830 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 51 ms. Found 1 JPA repository interfaces.
2023-06-08T15:43:19.014+02:00  INFO 2830 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 9.16.3 by Redgate
2023-06-08T15:43:19.014+02:00  INFO 2830 --- [           main] o.f.c.internal.license.VersionPrinter    : See release notes here: https://rd.gt/416ObMi
2023-06-08T15:43:19.014+02:00  INFO 2830 --- [           main] o.f.c.internal.license.VersionPrinter    : 
2023-06-08T15:43:19.316+02:00  INFO 2830 --- [           main] o.f.c.i.database.base.BaseDatabaseType   : Database: jdbc:postgresql://127.0.0.1:5432/postgres (PostgreSQL 15.2)
2023-06-08T15:43:19.392+02:00  INFO 2830 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 2 migrations (execution time 00:00.030s)
2023-06-08T15:43:19.463+02:00  INFO 2830 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema "public": 01.02
2023-06-08T15:43:19.465+02:00  INFO 2830 --- [           main] o.f.core.internal.command.DbMigrate      : Schema "public" is up to date. No migration necessary.
2023-06-08T15:43:19.596+02:00  INFO 2830 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-06-08T15:43:19.646+02:00  INFO 2830 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@709f0202
2023-06-08T15:43:19.648+02:00  INFO 2830 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-06-08T15:43:19.726+02:00  INFO 2830 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-06-08T15:43:19.773+02:00  INFO 2830 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.2.2.Final
2023-06-08T15:43:19.775+02:00  INFO 2830 --- [           main] org.hibernate.cfg.Environment            : HHH000406: Using bytecode reflection optimizer
2023-06-08T15:43:19.930+02:00  INFO 2830 --- [           main] o.h.b.i.BytecodeProviderInitiator        : HHH000021: Bytecode provider name : bytebuddy
2023-06-08T15:43:20.062+02:00  INFO 2830 --- [           main] o.s.o.j.p.SpringPersistenceUnitInfo      : No LoadTimeWeaver setup: ignoring JPA class transformer
2023-06-08T15:43:20.194+02:00  INFO 2830 --- [           main] org.hibernate.orm.dialect                : HHH035001: Using dialect: org.hibernate.dialect.PostgreSQLDialect, version: org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$DialectResolutionInfoImpl@582dcd35
2023-06-08T15:43:20.378+02:00  INFO 2830 --- [           main] o.h.b.i.BytecodeProviderInitiator        : HHH000021: Bytecode provider name : bytebuddy
2023-06-08T15:43:20.839+02:00  INFO 2830 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-06-08T15:43:20.844+02:00  INFO 2830 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-06-08T15:43:21.358+02:00  INFO 2830 --- [           main] c.a.d.BookRepositoryTest                 : Started BookRepositoryTest in 6.38 seconds (process running for 7.842)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
Hibernate: select count(*) from book b1_0
Hibernate: select nextval('book_seq')
Hibernate: insert into book (author,isbn,title,year,id) values (?,?,?,?,?)
Hibernate: select count(*) from book b1_0
Hibernate: delete from book where id=?
Hibernate: select count(*) from book b1_0
2023-06-08T15:43:22.450+02:00  INFO 2830 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-06-08T15:43:22.452+02:00  INFO 2830 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2023-06-08T15:43:22.456+02:00  INFO 2830 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

Process finished with exit code 0

5. Uso en proyectos donde se levantan varios contenedores

Hasta aquí hemos visto un caso muy sencillo donde tenemos un único fichero docker-compose que levanta los contenedores antes de ejecutar nuestros tests de integración, pero en muchos proyectos nos encontramos que no todos los tests necesitan tener levantados todos los contenedores, por ejemplo podemos tener un test que comprueba la integración con base de datos y otro test que valida el funcionamiento del broker de mensajería.

Podemos hacer uso de los perfiles de Spring para que cada test levante la configuración adecuada, para ello:

  • Bajo /src/test/resources/docker-compose/ tendremos tantos ficheros docker-compose como configuraciones de contenedores distintas necesitemos para nuestros tests.
  • Tendremos varios perfiles de Spring, cada uno de ellos definido en su fichero application-profileName.properties donde se referencia al fichero docker-compose correspondiente.
  • Por último, nuestros test especificarán el perfil Spring con el que se ejecutan mediante al anotacion @ActiveProfiles.

Ejemplo

Un aplicación que, por ejemplo, utilizase una base de datos PostgreSQL y un broker de mensajería Rabbit, podría tener la siguiente configuración para tests:

  • Bajo la carpeta /src/test/resources/docker-compose/ tendríamos 3 ficheros docker-compose:
    — docker-compose-postgresql.yml: configuración para levantar una base de datos PostgreSQL
    — docker-compose-rabbit.yml: configuración para levantar un broker Rabbit
    — docker-compose-postgresql-rabbit.yml: configuración para levantar una base de datos PostgreSQL y un broker Rabbit

  • Bajo la carpeta /src/test/resources/ tendríamos 3 ficheros con los diferentes perfiles de Spring, cada uno de ellos asigna valor a la propiedad spring.docker.compose.file para apuntar al fichero docker-compose correspondiente:
    — application-postgresql.properties: levanta una base de datos PostgreSQL
    — application-rabbit.properties: levanta un broker Rabbit
    — application-postgresql-rabbit.properties: levanta una base de datos PostgreSQL y un broker Rabbit

Finalmente, cada uno de los tests de integración de la aplicación se ejecutan con el perfil de Spring que interese para disponer de los contenedores con los que se debe integrar.

@DataJpaTest
@ActiveProfiles("postgresql")
class BookRepositoryTest {

    @Autowired
    private BookRepository sut;

    @Test
    void crud_book_ok() {
        assertEquals(1, sut.count());

        final Book book = new Book();
        book.setIsbn("123456789");
        book.setAuthor("Author 2");
        book.setTitle("Title 2");
        book.setYear(2023);
        sut.save(book);

        assertEquals(2, sut.count());

        sut.delete(book);

        assertEquals(1, sut.count());
    }
}

6. Conclusiones

Hemos explorado la capacidad que Spring Boot nos brinda para arrancar contenedores de forma nativa, sin depender de librerías externas o configuraciones personalizadas en plugins de Maven. Una de las principales ventajas de esta funcionalidad es que toda la configuración se lleva a cabo mediante un formato ampliamente conocido y aceptado dentro de la comunidad: YAML.

Esta característica nativa de Spring Boot para el arranque de contenedores elimina la necesidad de añadir complejidad adicional al proyecto. No se requiere la inclusión de bibliotecas externas o la escritura de configuraciones específicas en el archivo pom.xml del proyecto. Al utilizar un formato aceptado universalmente, podemos compartir fácilmente nuestras configuraciones de contenedores con otros desarrolladores y equipos, lo que facilita la colaboración y el intercambio de conocimientos.

Consultor tecnológico de desarrollo de proyectos informáticos.
Ingeniero en informática por la Universidad de Oviedo.
Puedes encontrarme en Autentia: Ofrecemos servicios de soporte a desarrollo, factoría y formación.
Somos expertos en Java/Java EE

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad