CodeGym /Cursos /Spring Test /Ruta de reserva: @DynamicPropertySource

Ruta de reserva: @DynamicPropertySource

Spring Test
Nivel 25, Lección 2
Disponible

1. El papel de @DynamicPropertySource y @ServiceConnection

Si acabas de enamorarte de @ServiceConnection con todo tu corazón, te entiendo: menos código, menos errores, menos posibilidades de olvidar accidentalmente la contraseña. Pero las pruebas tienen una particularidad: parte de la configuración solo se conoce durante la ejecución. El contenedor levanta PostgreSQL en un puerto aleatorio, genera los parámetros de conexión, y esos valores deben "inyectarse" en Spring antes de que cree el datasource.

Es decir, la cuestión ya no es si el contenedor es necesario o no. Para un datasource PostgreSQL normal, @ServiceConnection sigue siendo la primera opción. @DynamicPropertySource es necesario cuando el cableado automático ya no es suficiente y los valores de tiempo de ejecución deben colocarse manualmente en el Environment.

En un mundo ideal, Spring Boot lo recogería todo por sí mismo. Y en un caso típico con PostgreSQL, efectivamente lo hace a través de la conexión de servicio. Pero hay situaciones en las que el cableado automático no funciona o no es adecuado: necesitas claves de propiedad no estándar, quieres vincular el contenedor no con un datasource sino con otro conjunto de configuraciones, o necesitas poner manualmente varios valores dinámicos adicionales en el Environment. Entonces hay que volver a un mecanismo más universal de Spring Test: a @DynamicPropertySource.

Es útil pensar en ello así: @ServiceConnection es la "carretera principal", y @DynamicPropertySource es el "camino de tierra" que lleva al mismo lugar, pero te permite sortear casi cualquier obstáculo. El camino de tierra es más largo y más sucio (el código es más extenso), pero tú controlas todo.

2. Cómo funciona @DynamicPropertySource

Cuando oyes "fuente de propiedades dinámica", podrías pensar que va a haber magia, rituales y la invocación de los espíritus de Spring. En realidad, la idea es muy terrenal: Spring Test te proporciona un punto donde puedes agregar propiedades mediante código en el Environment de Spring justo antes de construir el ApplicationContext. Es decir, no a través de application-test.yml, no a través de properties = ... en la anotación, sino mediante código.

Técnicamente se ve así. En la clase de prueba declaras un método static y lo marcas con @DynamicPropertySource. Spring encuentra este método al preparar el contexto de prueba y lo invoca, pasándole un objeto DynamicPropertyRegistry. Y tú agregas pares "clave → proveedor de valor" en este registro. Y aquí hay un detalle importante: no estás obligado a devolver el valor inmediatamente. Para nosotros es mucho más conveniente devolver un proveedor (Supplier), porque el contenedor puede iniciarse un poco más tarde, y entonces postgres::getJdbcUrl se calculará en el momento adecuado.

A continuación se muestra un esquema (un poco simplificado, pero fiel en esencia) de cómo ocurre esto cuando conectamos Testcontainers a través de @DynamicPropertySource:

flowchart TD
    A["JUnit 5 comienza a ejecutar la clase de prueba"] --> B["Testcontainers prepara el contenedor (PostgreSQLContainer)"]
    B --> C["Spring Test prepara el ApplicationContext"]
    C --> D["@DynamicPropertySource registra propiedades dinámicas en DynamicPropertyRegistry"]
    D --> E["Spring vincula las propiedades (Environment)"]
    E --> F["Se crea la infraestructura DataSource / Flyway / JPA"]
    F --> G["Las pruebas se ejecutan y trabajan con PostgreSQL real"]

Ahora, hablemos de los requisitos que suelen "morder" a los principiantes:

El método con @DynamicPropertySource debe ser static. No es un capricho, sino una consecuencia directa de que las propiedades deben registrarse antes de crear una instancia de la clase de prueba. Si el método no es static, Spring simplemente no podrá llamarlo de forma segura durante el arranque y obtendrás un error del tipo "el método debe ser estático".

DynamicPropertyRegistry no es un "mapa de configuración de la aplicación", sino precisamente un registro de valores dinámicos. El estilo más correcto es agregar solo aquello que realmente depende del tiempo de ejecución: URL, puerto, usuario/contraseña, alguna dirección temporal. Si empiezas a poner de todo allí, incluidas constantes, convertirás rápidamente @DynamicPropertySource en un vertedero (y luego te preguntarás por qué nadie quiere tocar tu código de prueba sin casco).

3. Ejemplo de @DataJpaTest con Postgres

Ahora vamos a construir un ejemplo mínimo, "legible a simple vista", para ContentHub. Necesitamos un @DataJpaTest que funcione con PostgreSQL en un contenedor. Lo principal en este escenario no es la prueba en sí, sino el cableado: el contenedor debe levantarse y Spring debe recibir las propiedades spring.datasource.* correctas.

Para no construir un segundo mundo separado, tomaremos el mismo ArticleRepositoryPostgresContainerTest que en la variante de ruta preferida. Solo cambia la forma en que las propiedades spring.datasource.* llegan al contexto.

import javax.sql.DataSource;

import org.junit.jupiter.api.Test;
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.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@Testcontainers // Activamos la integración de Testcontainers con el ejecutor de pruebas
@DataJpaTest // Prueba de slice de datos: levantamos solo la parte JPA del contexto
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // No reemplazamos el DataSource por una BD embebida
class ArticleRepositoryPostgresContainerTest {

    @Container // El contenedor es gestionado por el ciclo de vida de Testcontainers
    static PostgreSQLContainer
   postgres =
            new PostgreSQLContainer<>("postgres:16-alpine"); // Imagen de PostgreSQL para las pruebas

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        // Importante: pasamos un Supplier para que el valor se calcule cuando el contenedor ya esté listo
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private DataSource dataSource;

    @Test // JUnit 5
    void connectsToPostgres() throws Exception {
        // Verificamos la conexión real: si falla aquí, el cableado no funcionó
        try (var c = dataSource.getConnection()) {
            assertThat(c.isValid(1)).isTrue(); // 1 segundo de tiempo de espera para la verificación
        }
    }
}

Observa dos cosas. Primero, replace = NONE sigue ahí: @DynamicPropertySource no anula las reglas del slice de datos, simplemente reemplaza el puente automático por un registro manual de propiedades. Segundo, esta es la misma forma de prueba que con @ServiceConnection: el contenedor sigue siendo static, la clase sigue siendo @DataJpaTest, solo cambia el cableado del datasource.

Si quieres un poco más de tranquilidad con respecto al ciclo de vida, ten esta regla simple: no calcules los valores demasiado pronto. postgres::getJdbcUrl es más seguro que intentar extraer la cadena de antemano y guardarla "en algún lugar cercano".

Propiedades dinámicas adicionales

En los primeros pasos parece que el datasource lo es todo. Pero un proyecto real tiende a añadir "ejes secundarios". Por ejemplo, tienes Flyway activado, y puede usar configuraciones separadas diferentes a las del datasource. O quieres que el registro de SQL solo se active en el subconjunto basado en contenedores. O necesitas colocar un puerto dinámico en el cliente de integración, y todo esto se puede hacer cómodamente con el mismo mecanismo.

A continuación se muestra un ejemplo, no de una nueva clase, sino de una variante de reemplazo para el método postgresProperties dentro del mismo ArticleRepositoryPostgresContainerTest. En la mayoría de los proyectos, Flyway tomará la configuración del datasource de todas formas, así que no es un patrón de uso permanente. Pero ilustra bien la idea: DynamicPropertyRegistry puede llenar cualquier clave, no solo spring.datasource.*.

// Variante de reemplazo para el método postgresProperties dentro del mismo ArticleRepositoryPostgresContainerTest
@DynamicPropertySource
static void postgresAndFlyway(DynamicPropertyRegistry registry) {
    // Configuración básica del DataSource para Spring Boot
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);

    // Pasamos explícitamente los mismos parámetros a Flyway (a veces es útil para escenarios no estándar)
    registry.add("spring.flyway.url", postgres::getJdbcUrl);
    registry.add("spring.flyway.user", postgres::getUsername);
    registry.add("spring.flyway.password", postgres::getPassword);
}

Aquí es importante no caer en el extremo de "ya que tengo DynamicPropertyRegistry, pondré allí la mitad de application.yml". Es mejor mantener una regla simple: dinámico significa que depende del tiempo de ejecución. Si un valor es estático, es más simple y honesto definirlo a través de application-test.yml o mediante properties = ... en la anotación de la prueba. Así conservarás la legibilidad y no convertirás las propiedades dinámicas en un "lugar secreto donde viven todas las configuraciones".

4. @ServiceConnection vs @DynamicPropertySource

Por supuesto, uno quisiera declarar @DynamicPropertySource como "obsoleto" porque existe @ServiceConnection. Pero eso no sería cierto: tienen objetivos diferentes. @ServiceConnection ofrece un camino corto, pero funciona mejor en escenarios estándar y para tipos de contenedores/vínculos compatibles. @DynamicPropertySource es un destornillador más universal: puede que no sea tan bonito, pero casi siempre sirve.

@ServiceConnection sigue siendo la primera opción para un datasource PostgreSQL normal. @DynamicPropertySource es necesario precisamente cuando el puente estándar ya no es suficiente.

Los comparamos en una tabla: es más fácil de recordar que diez párrafos de "en general es así":

Criterio @ServiceConnection @DynamicPropertySource
Tamaño del código Generalmente mínimo Generalmente más código repetitivo
Legibilidad Alta: "contenedor → conexión de servicio" Media: hay que conocer las claves de propiedad
Flexibilidad Buena, mientras estés en el "estándar" Muy alta: se pueden registrar cualquier propiedad
Claves no estándar Incómodo/imposible sin puentes adicionales Justo para eso está hecho
Riesgo de copiar y pegar Bajo Alto (si repites registry.add en cada prueba)
Estado principal en el curso Ruta preferida Ruta de reserva

Si trabajas con ContentHub y necesitas un datasource PostgreSQL normal en una prueba, tu primera opción es @ServiceConnection. Pero si de repente descubres que parte de la configuración de la aplicación se "engancha" con claves personalizadas (o Boot no puede vincular tu contenedor con el subsistema necesario), @DynamicPropertySource se convierte en una solución honesta y comprensible. Es importante recordar: una herramienta de reserva no significa "mala". Significa "más manual", y por lo tanto requiere más disciplina.

5. Cómo evitar el copiar y pegar

@DynamicPropertySource tiene un efecto secundario: provoca copiar y pegar. La primera clase de prueba está bien. La segunda ya da ganas de "copiar rápido". La tercera, y de repente te das cuenta de que ha aparecido un nuevo tipo de deuda técnica en tu proyecto: "las propiedades dinámicas están dispersas por las pruebas".

El remedio más simple es extraer el mapeo de propiedades a un pequeño helper. Es importante que este helper no se convierta en un enorme "marco de pruebas con tu nombre" que lo sabe hacer todo excepto explicar qué está pasando.

Ejemplo de una clase helper minimalista: no oculta el contenedor ni gestiona su ciclo de vida, solo ayuda a registrar las claves de la misma manera.

import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.containers.PostgreSQLContainer;

final class PostgresDynamicProperties {

    static void register(DynamicPropertyRegistry registry, PostgreSQLContainer
   postgres) {
        // Registramos solo lo que realmente cambia de una ejecución a otra (valores de tiempo de ejecución)
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    private PostgresDynamicProperties() {
        // Clase de utilidad: no se necesitan instancias
    }
}

Y en la clase de prueba queda un "punto de entrada" honesto, donde se ve lo que está sucediendo:

import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

@DynamicPropertySource
static void postgresProps(DynamicPropertyRegistry registry) {
    // En la prueba se ve: simplemente pasamos los valores del contenedor a las claves estándar de Spring Boot
    PostgresDynamicProperties.register(registry, postgres);
}

¿Por qué es un buen compromiso para nuestro curso? Porque nuestro objetivo no es construir una infraestructura ultra universal, sino mantener el subconjunto basado en contenedores pequeño y comprensible. Queremos eliminar las líneas repetitivas, pero no queremos que el estudiante lea la prueba y piense: "espera, ¿quién establece el datasource? ¿dónde? ¿por qué funciona?". El helper ahorra líneas y deja el significado cerca de la prueba.

6. Errores típicos con @DynamicPropertySource

Error nº 1: el método con @DynamicPropertySource no es static.
Es un clásico: el estudiante escribe un método bonito void postgresProperties(...), y Spring responde de forma bastante tajante: "no". Las propiedades dinámicas son necesarias antes de crear una instancia de la prueba, por lo tanto el método debe ser static. No es un "detalle de versión", es un requisito conceptual.

Error nº 2: registran un "valor" en lugar de un Supplier, y accidentalmente lo calculan demasiado pronto.
A veces se quiere hacer "más simple": llamar a postgres.getJdbcUrl() y pasar la cadena. En primer lugar, la API del registry está diseñada en torno a Supplier. En segundo lugar, incluso si lo evitas con una lambda, el cálculo temprano del valor puede ocurrir antes de que el contenedor se inicie y provocar fallos muy extraños. El estilo más seguro son las referencias a métodos: postgres::getJdbcUrl, postgres::getUsername, postgres::getPassword.

Error nº 3: usan simultáneamente @ServiceConnection y @DynamicPropertySource para el mismo datasource.
Es el caso en el que intentas ponerte el cinturón de seguridad encima de otro cinturón de seguridad y accidentalmente te abrochas a la puerta. Si @ServiceConnection ya está en el contenedor, no es necesario registrar manualmente las mismas claves spring.datasource.*; aumentas el riesgo de conflicto y haces la prueba menos predecible. Elige un camino por clase de prueba.

Error nº 4: meten de todo en DynamicPropertyRegistry, incluidas configuraciones estáticas.
Cuando el registry se convierte en un "application-test.yml alternativo", la prueba se vuelve ilegible: los valores importantes dependientes del tiempo de ejecución se ahogan entre constantes. Las cosas estáticas (por ejemplo, logging.level.*) es mejor mantenerlas en YAML o mediante anulaciones locales en las anotaciones. El registry dinámico es para lo realmente dinámico.

Error nº 5: se olvidan de @AutoConfigureTestDatabase(replace = NONE) en @DataJpaTest y luego se sorprenden.
El síntoma suele ser: el contenedor arranca alegremente, ves los logs de PostgreSQL, pero las pruebas se comportan como si no hubiera base de datos. La causa es que Spring Boot reemplazó el datasource por una BD embebida porque no le dijiste "no hace falta que ayudes". En un @DataJpaTest basado en contenedor, esta línea es casi obligatoria.

1
Tarea
Spring Test, nivel 25, lección 2
Bloqueada
Registro manual de datasource mediante `@DynamicPropertySource`
Registro manual de datasource mediante `@DynamicPropertySource`
1
Tarea
Spring Test, nivel 25, lección 2
Bloqueada
Extracci\u00f3n de dynamic properties a una clase helper
Extracci\u00f3n de dynamic properties a una clase helper
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION