CodeGym /Cursos /Spring Test /@AutoConfigureTestDatabase: replace = NONE

@AutoConfigureTestDatabase: replace = NONE

Spring Test
Nivel 16, Lección 3
Disponible

1. La base de datos embebida a veces miente

Mientras aprendes a escribir @DataJpaTest, la base de datos embebida parece un compañero ideal: se inicia rápido, no es necesario levantar nada manualmente, las pruebas vuelan como un avión a reacción. Pero esta idílica situación tiene un efecto secundario: la base de datos embebida no es un «pequeño PostgreSQL», sino una base completamente diferente, con su propio carácter, hábitos y comprensión de las palabras «unicidad», «ordenamiento» y «fecha».

Y aquí comienza la situación clásica de la vida real: las pruebas están verdes, estás contento, y luego la aplicación se conecta a PostgreSQL real — y de repente resulta que alguna consulta devuelve resultados en otro orden, la migración se comporta de manera diferente, y un error de restricción aparece donde no lo esperabas. No es porque Spring se haya «roto». Es porque probaste el comportamiento en un «simulador», y en la realidad saliste al «ring real».

La base de datos embebida es una herramienta honesta para la velocidad y la retroalimentación. Solo es importante recordar: demuestra la corrección de la lógica del repositorio en general, pero no siempre demuestra la corrección del comportamiento específico de PostgreSQL, con el que vive tu ContentHub en producción.

Aquí es importante no perder la escala: no estamos cambiando el tipo de prueba ni construyendo una pista de infraestructura separada. Sigue siendo el mismo @DataJpaTest para la capa de repositorio; solo cambia la cuestión de qué base de datos está debajo y cuándo el entorno mismo se convierte en parte del riesgo.

2. @DataJpaTest y la sustitución de DataSource

@DataJpaTest: de dónde viene la base de datos

Para gestionar conscientemente la elección de la base, es útil entender: @DataJpaTest es un slice. Levanta la infraestructura JPA y los repositorios, pero al mismo tiempo Spring Boot intenta hacer la prueba lo más «ligera» e independiente posible. Una de las formas es reemplazar automáticamente tu DataSource por uno embebido, si está disponible en el classpath.

En la práctica se ve así: en tu aplicación tienes configurado PostgreSQL en application.yml, pero en las pruebas, si hay una dependencia de DB embebida (a menudo H2), Boot dice: «Oh, tenemos una base en memoria, déjame conectarla para que las pruebas no dependan de un servicio externo». Y la conecta.

Si lo representamos como un pequeño diagrama, quedaría algo así:

flowchart TD
    A["@DataJpaTest inicia"] --> B{¿Hay driver de DB embebida en classpath?}
    B -- "Sí" --> C["Boot reemplaza DataSource con base de datos embebida"]
    B -- "No" --> D["Boot usa DataSource de la configuración de prueba/proyecto"]
    C --> E["Las pruebas corren rápido, pero la base puede diferir de PostgreSQL"]
    D --> F["Las pruebas están más cerca del entorno de producción, pero requieren una BD real"]

Y aquí aparece la idea clave de la lección: a veces no necesitas simplemente «alguna base de datos para la prueba», sino tu propia base (por ejemplo PostgreSQL), porque el riesgo de defecto reside en las particularidades del dialecto, tipos y comportamiento.

@AutoConfigureTestDatabase: gestión de la sustitución de DataSource

La anotación @AutoConfigureTestDatabase no trata sobre JPA, ni sobre repositorios, ni sobre un «súper modo de pruebas». Es algo muy terrenal: controla si Spring Boot intentará reemplazar DataSource por una base de datos embebida.

Punto importante: @DataJpaTest normalmente ya incluye el comportamiento de «reemplazar la base si es posible». Por lo tanto, cuando añades @AutoConfigureTestDatabase(replace = NONE), no estás «activando una BD real», estás prohibiendo el reemplazo.

@AutoConfigureTestDatabase tiene modos Replace. Ahora nos interesan principalmente dos: el predeterminado (reemplazo permitido) y NONE (reemplazo prohibido). Para no tener esto en la cabeza como un hechizo, es conveniente verlo una vez en la tabla:

Modo Replace Lo que Boot hace en la prueba Interpretación simple
ANY Pone DB embebida si está disponible Si se puede simplificar, simplificamos
NONE No reemplaza DataSource Prueba en la base que está configurada

En la práctica, la transición se ve así: antes (ruta embebida por defecto), después (ruta de BD real a través de configuración).

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // Levantamos solo el corte JPA: repositorios e infraestructura, sin toda la aplicación
class ArticleRepositoryDataJpaTest {
    // Aquí Boot puede reemplazar DataSource por BD embebida (si está en classpath)
}

Y la opción «no reemplazar»:

import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // Todavía es un JPA-slice, no un @SpringBootTest completo
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // Prohibimos a Boot reemplazar DataSource por BD embebida
class ArticleRepositoryRealDbTest {
    // Después de esto, DataSource se tomará de tu configuración (por ejemplo, perfil test)
}

Observa un detalle psicológico importante: sigue siendo @DataJpaTest. No has convertido la prueba en @SpringBootTest. Todavía estás probando la capa de persistencia de manera estrecha y relativamente rápida. Simplemente cambiaste «qué base de datos está debajo».

Y esto no es un nuevo predeterminado para todo el suite. La mayoría de las pruebas de repositorio siguen viviendo perfectamente en la ruta embebida y dan retroalimentación rápida; replace = NONE es necesario donde el riesgo reside precisamente en el comportamiento de la base concreta.

3. replace = NONE en la práctica

La idea más útil aquí es un poco aburrida, pero muy ingenieril: al cambiar a replace = NONE, la lógica de la prueba generalmente no cambia. No debes reescribir de repente las aserciones, añadir nuevas comprobaciones o cambiar la estructura AAA. No cambias la prueba, sino el entorno en el que se ejecuta.

Si la prueba verificaba «el repositorio encuentra un artículo publicado por slug», entonces sigue verificando lo mismo. Solo que ahora recibes la respuesta no de H2/HSQLDB/Derby, sino de PostgreSQL real (o de la BD que tengas configurada como base real del proyecto).

Aquí un ejemplo al estilo ContentHub, lo más terrenal posible. Supongamos que tenemos un método que lee un artículo por slug y estado:

import java.util.Optional;

import org.springframework.data.repository.Repository;

interface ArticleRepository extends Repository<Article, Long> {

    // Buscamos artículo por clave de negocio (slug) y estado de publicación
    Optional<Article> findBySlugAndStatus(String slug, ArticleStatus status);
}

Y ahora la prueba. Se ve igual tanto para la ruta embebida como para la ruta de BD real:

import org.junit.jupiter.api.Test; 
import org.springframework.beans.factory.annotation.Autowired;

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

class ArticleRepositoryExampleTest {

    @Autowired
    ArticleRepository repository; // Probamos el repositorio directamente, sin servicio/controlador

    @Test
    void findsPublishedArticleBySlug() {
        // Acción: llamamos al método del repositorio como lo haría el código real
        var result = repository.findBySlugAndStatus(
                "spring-boot-testing",
                ArticleStatus.PUBLISHED
        );

        // Verificación: para el ejemplo de aprendizaje es suficiente asegurarse de que se encuentra el artículo
        assertThat(result).isPresent();
    }
}

Sí, en realidad necesitas preparar los datos a través de TestEntityManager.persist(...), luego hacer flush() y clear(). Pero el sentido es otro: la prueba en sí no se convierte en «otra prueba». Simplemente obtiene una evidencia más sólida.

Y otro punto que suele sorprender a los principiantes: las pruebas con replace = NONE en @DataJpaTest siguen siendo normalmente transaccionales y se revierten. Es decir, no estás «ensuciando» la base con datos infinitos después de cada prueba — mientras no tengas un commit intencionado y configuraciones especiales.

4. En qué se diferencia la DB embebida de PostgreSQL

Cuando se dice «la base de datos embebida puede engañar», no significa que «devuelva datos incorrectos al azar». Normalmente se trata de que algunos matices límite se comportan de manera diferente. En un proyecto real, son precisamente esos matices los que se convierten en regresiones, porque son difíciles de ver a simple vista en el código y fáciles de pasar por alto en la revisión.

Una de las clases más comunes de divergencias es DDL y constraints. Tus migraciones (Flyway) y el esquema en PostgreSQL pueden ser más ricos: tipos, restricciones, índices, matices de longitudes varchar, comportamiento de unique constraints. La DB embebida a veces perdona lo que PostgreSQL no perdonará, o viceversa: «se queja» de DDL que PostgreSQL acepta tranquilamente. El resultado es que obtienes una falsa confianza o una falsa alarma.

La segunda gran zona son los tipos de fecha/hora y su precisión. PostgreSQL y las bases embebidas almacenan timestamps de manera diferente, redondean microsegundos/nanosegundos de manera diferente, tratan la zona horaria de manera diferente. Si en ContentHub el orden de las publicaciones depende de publishedAt, puedes encontrarte con un error muy desagradable: la ordenación «flota» porque en una base el tiempo se almacena con una precisión y en otra con otra. Es especialmente traicionero cuando creas muchos registros «casi al mismo tiempo».

La tercera zona es la ordenación, NULL y la estabilidad del orden. PostgreSQL tiene sus propias reglas de ordenación de valores NULL (por ejemplo, en algunos modos NULL puede ser «el más grande» e ir al final/principio dependiendo de la dirección de la ordenación). La DB embebida puede ordenar de manera diferente. Si tu query construye un feed público y esperas un orden estable, las diferencias en NULLS FIRST/LAST o en la comparación de cadenas pueden manifestarse solo en la base real.

La cuarta zona es la generación de ID y secuencias. JPA intenta abstraerte de cómo se generan exactamente las claves, pero en la práctica IDENTITY, SEQUENCE y «cómo la base realmente crea valores» a veces afecta el orden de inserción, cuándo aparece exactamente el ID, y algunos escenarios límite en torno a operaciones por lotes. Esto rara vez «rompe todo», pero a veces rompe pruebas que esperaban un orden de acciones y obtuvieron otro.

Y finalmente, la quinta zona es el dialecto y las características SQL. JPQL suele ser portable, pero el SQL generado (especialmente para paginación, limit/offset, ordenación, algunas funciones) depende del dialecto. En DB embebida podrías obtener un SQL y comportamiento, y en PostgreSQL otro. Si estás probando una consulta importante (por ejemplo, la entrega pública de artículos publicados), estas diferencias precisamente deberían ser detectadas lo más cerca posible de la realidad.

Importante: no es necesario memorizar este párrafo como una lista de hechizos. Basta con una idea: las diferentes bases tienen diferentes costumbres, y replace = NONE permite verificar el comportamiento en la base que es importante para tu proyecto.

5. Configuración mínima de BD real

La propia anotación @AutoConfigureTestDatabase(replace = NONE) no crea PostgreSQL de la nada. Solo le dice a Boot: «no reemplaces el datasource». Por lo tanto, debes entender de dónde vendrá DataSource si ya no hay reemplazo embebido.

Normalmente en un proyecto de aprendizaje esto se resuelve mediante el perfil test, que ya has visto: application-test.yml en src/test/resources o src/main/resources (dependiendo de las reglas del proyecto), y la activación explícita del perfil test. Aquí no discutimos cómo levantar PostgreSQL (Docker, Compose, etc.) — simplemente fijamos el principio: la prueba se conectará al datasource que hayas configurado.

Un ejemplo ilustrativo de src/test/resources/application-test.yml para PostgreSQL podría verse así:

spring:
  datasource:
    # JDBC URL de la BD de prueba: las pruebas se conectarán aquí
    url: jdbc:postgresql://localhost:5432/contenthub
    username: contenthub
    password: contenthub
  jpa:
    hibernate:
      # En pruebas es útil validar el esquema para detectar discrepancias con las migraciones
      ddl-auto: validate

Si usas perfiles (y en el curso los usamos), la prueba a menudo necesita @ActiveProfiles("test") para que este YAML se aplique realmente:

import org.springframework.test.context.ActiveProfiles;

@ActiveProfiles("test") // Activamos el perfil test para que se recoja application-test.yml
class SomeDataTest {
    // Aquí no se muestran las anotaciones del slice de prueba — lo importante es el perfil
}

Una vez más: ahora no es importante «configurarlo todo perfectamente», sino entender que replace = NONE hace la prueba más honesta, pero requiere un entorno más honesto por tu parte. De lo contrario, el contexto simplemente puede no levantarse porque no encontró un datasource funcional.

6. Errores típicos con replace = NONE

Error nº1: percibir replace = NONE como «haz la prueba profesional».
A veces parece que basta con añadir una anotación y listo, eres un «desarrollador adulto». En la práctica, es simplemente un interruptor de entorno. Si la prueba en sí es débil (por ejemplo, solo verifica hasSize(2) sin verificar contenido y orden), entonces en PostgreSQL seguirá siendo igual de débil — solo se ejecutará más lento y fallará más ruidosamente.

Error nº2: confundir «base real» con «contexto completo de la aplicación».
@DataJpaTest con replace = NONE sigue siendo un data-slice. No estás probando controladores, servicios ni seguridad. Si la prueba de repente comienza a «exigir inesperadamente» la mitad de la aplicación, significa que accidentalmente saliste de los límites de la capa (por ejemplo, importaste configuración extra o trajiste dependencias que no son del mundo de persistencia).

Error nº3: cambiar las aserciones para que la prueba «pase» en la nueva base.
Un hábito muy peligroso: cambiaste a PostgreSQL, la prueba falló debido al comportamiento real, y en lugar de analizar simplemente «ajustaste la expectativa». Así es fácil legalizar un error. Si el resultado cambia, primero formula por qué. ¿Es un defecto? ¿Una peculiaridad? ¿Un order-by incorrecto? ¿Un problema con NULL? Solo entonces cambia la prueba o el código.

Error nº4: olvidar que ahora el datasource se toma de la configuración — y obtener un fallo del contexto.
En la ruta embebida muchas cosas funcionaban «de fábrica», y podías no pensar de dónde venía la base. Con replace = NONE, la prueba intentará honestamente conectarse al datasource real. Si no hay configuración o la base no está disponible, la prueba fallará al inicio. Es normal: la herramienta no está rota, simplemente pediste la realidad.

Error nº5: trasladar todo a la ruta de BD real, incluidas las consultas más simples.
Incluso si quieres «máxima honestidad», el costo de las pruebas es importante. La base real suele ser más lenta, requiere más disciplina del entorno y puede empeorar la velocidad de retroalimentación. Es mucho más razonable primero entender qué es lo que realmente presenta riesgo, y luego decidir qué pruebas deberían vivir realmente con replace = NONE. (Alto — no vamos más allá para no adelantarnos al tema del día.)

1
Tarea
Spring Test, nivel 16, lección 3
Bloqueada
Prueba de data-slice sin sustitución de DataSource
Prueba de data-slice sin sustitución de DataSource
1
Tarea
Spring Test, nivel 16, lección 3
Bloqueada
Verificación de unicidad de slug en real DB path
Verificación de unicidad de slug en real DB path
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION