Día 20 de 30 Fase 5: Testing de Integración, APIs y Carga

Testing de Integración Real con Testcontainers y PostgreSQL

Pruebas de Extremo a Extremo sin Depender de Simuladores H2 en Memoria

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 4h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Java 17 LTSJava 21 LTSTestcontainersPostgreSQLDocker@SpringBootTestAssertJ
Al terminar este día sabrás 4
  • Justificar por qué los tests de integración deben usar la misma base de datos que producción.
  • Levantar PostgreSQL en tests con Testcontainers y @ServiceConnection.
  • Elegir entre test de rebanada y test completo según lo que quieres verificar.
  • Mantener rápida una suite grande reutilizando contenedores y contextos.

El Peligro de los Falsos Positivos con H2 en Memoria

H2 tiene dialectos, funciones de bloqueo, tipos de datos JSON y comportamientos transaccionales diferentes a PostgreSQL o MySQL. Un test que pasa verde en H2 puede fallar en producción.

En una línea Levantar un contenedor real de PostgreSQL 16 con Testcontainers.

Consultas que H2 no reproduce fielmente sql
-- Upsert: en H2 la sintaxis y el comportamiento difieren
INSERT INTO etiqueta (nombre, usos) VALUES ('java', 1)
ON CONFLICT (nombre) DO UPDATE SET usos = etiqueta.usos + 1;

-- JSONB con operadores e índice GIN
SELECT id FROM tarea WHERE metadatos @> '{"origen": "jira"}';

-- Bloqueo pesimista saltando filas bloqueadas (colas de trabajo)
SELECT id FROM trabajo WHERE estado = 'PENDIENTE'
ORDER BY creado_en LIMIT 10
FOR UPDATE SKIP LOCKED;

-- Búsqueda sin distinguir mayúsculas
SELECT * FROM tarea WHERE titulo ILIKE '%spring%';
Cómo funciona por dentro

H2 en memoria es rápido y no necesita instalación, pero no es PostgreSQL. Incluso en modo de compatibilidad difiere en tipos (JSONB, arrays, UUID, rangos), funciones (ON CONFLICT, ILIKE, funciones de ventana avanzadas, generate_series), en cómo trata mayúsculas, en el bloqueo y en los niveles de aislamiento.

El resultado son falsos positivos: tests en verde con consultas que fallan en producción, y falsos negativos: código válido para PostgreSQL que no puedes probar porque H2 no lo entiende. Además, tus migraciones Flyway escritas para PostgreSQL a menudo no se ejecutan en H2, así que acabas manteniendo dos esquemas.

Testcontainers arranca la imagen oficial de PostgreSQL (la misma versión que producción) en Docker en pocos segundos. La confianza que da un test contra el motor real compensa con creces el coste de arranque.

Garantiza que el software se pruebe exactamente contra el mismo motor de base de datos que corre en producción.

Se conecta con Testing de IntegraciónDockerPostgreSQL

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Configurar Testcontainers y Docker (0 - 25 min)

    Qué estudiar. Dependencias en pom.xml: testcontainers y postgresql.

    Pregunta a responder. ¿Cómo verificar que Docker está corriendo y accesible para Java?

    Resultado esperado. Test básico que arranca un contenedor de PostgreSQL y verifica que está isRunning().

  2. Bloque 2: Integración con Spring Boot (@ServiceConnection) (25 - 50 min)

    Qué estudiar. Anotación @ServiceConnection en Spring Boot 3.1+.

    Pregunta a responder. ¿Cómo evitar escribir @DynamicPropertySource a mano?

    Resultado esperado. Test @SpringBootTest que inserta un registro y lo lee de PostgreSQL en Docker.

  3. Bloque 3: Verificación con Flyway (50 - 80 min)

    Qué estudiar. Ejecución automática de migraciones Flyway sobre el contenedor efímero.

    Pregunta a responder. ¿Cómo asegurar que el esquema completo se despliega en el test?

    Resultado esperado. Suite de pruebas completa verificando persistencia real.

  4. Cierre y Reflexión (80 - 90 min)

    Qué estudiar. Repaso de la pirámide de testing.

    Pregunta a responder. ¿Qué porcentaje de tests en tu suite deben ser unitarios vs integración?

    Resultado esperado. Progreso guardado en la app.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué Testcontainers revolucionó el mundo de Java

Lo que debes entender

Antes de Testcontainers, para correr tests de integración necesitabas que cada desarrollador tuviera un PostgreSQL instalado y configurado a mano en su máquina con los mismos usuarios y puertos. Si alguien tenía otra versión, el test fallaba. Testcontainers automatiza esto con Docker: levanta una BD limpia y efímera en un puerto aleatorio y la destruye al terminar.

Analogía para contarlo

Alquilar un coche limpio en el aeropuerto para hacer tu viaje y devolverlo al terminar, en lugar de obligar a cada empleado a comprarse el mismo modelo de coche para ir a trabajar.

Lo que suele explicarse mal

Explicarlo como "un plugin de Maven", cuando en realidad es una librería Java que se comunica con la API de Docker mediante su socket local.

Señal de que aún no lo dominas

Si intentas correr Testcontainers en una máquina sin el daemon de Docker iniciado.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué Testcontainers asigna puertos aleatorios en el host en lugar del puerto 5432 estándar de PostgreSQL? Ver respuesta

Respuesta razonada

Para evitar colisiones. Si el contenedor publicara siempre el 5432 del host, chocaría con un PostgreSQL instalado en tu máquina, con otro proyecto ejecutando tests a la vez o con dos tests en paralelo en el mismo agente de integración continua. Con un puerto libre aleatorio, cualquier número de contenedores puede convivir.

Dentro del contenedor PostgreSQL sigue escuchando en 5432; Testcontainers mapea ese puerto a uno libre del host y lo expone con getMappedPort(5432) o directamente en getJdbcUrl(). Por eso nunca se escribe la URL a mano en los tests: se obtiene del contenedor, y con @ServiceConnection ni siquiera eso.

Qué está evaluando la pregunta

Para evitar colisiones si el desarrollador ya tiene un PostgreSQL corriendo en su máquina local en el puerto 5432 y para permitir ejecutar múltiples builds de integración en paralelo.

Nivel 2 (Intermedio) ¿Cómo interactúa Flyway con Testcontainers en un test @SpringBootTest? Ver respuesta

Respuesta razonada

Flyway se autoconfigura al arrancar el contexto de Spring y usa el mismo DataSource, que con @ServiceConnection apunta al contenedor. El orden es: Testcontainers arranca PostgreSQL vacío, Spring crea el DataSource, Flyway aplica todas las migraciones de db/migration y después se crean los beans de JPA (que con ddl-auto=validate comprueban el esquema).

Esto tiene un valor añadido: cada ejecución de los tests valida tus migraciones reales contra la versión real de PostgreSQL. Un error de sintaxis en V7__...sql se detecta en tu máquina, no al desplegar. Si el contenedor se comparte entre clases, las migraciones se aplican una sola vez y los datos de prueba hay que limpiarlos entre tests.

Qué está evaluando la pregunta

Al arrancar el contexto de prueba, Spring Boot se conecta al contenedor de PostgreSQL real y Flyway ejecuta automáticamente todos los scripts de migración V1, V2... verificando que el esquema real de producción se cree limpiamente.

Nivel 3 (Entrevista Senior) ¿Qué es el patrón "Singleton Container" en Testcontainers y por qué se utiliza en suites de prueba grandes? Ver respuesta

Respuesta razonada

Consiste en arrancar los contenedores una sola vez por JVM, normalmente en un bloque static de una clase base abstracta de la que heredan todos los tests de integración, sin la anotación @Container (que los pararía al terminar cada clase). Testcontainers lanza además un contenedor auxiliar llamado Ryuk que elimina todo cuando la JVM termina, así que no quedan restos.

Se usa porque en suites grandes el coste dominante es arrancar contenedores: con 80 clases de test, pasar de 80 arranques de PostgreSQL a uno ahorra minutos en cada ejecución. El precio es que el estado se comparte, así que cada test debe limpiar o aislar sus datos (borrar tablas, usar identificadores únicos o confiar en el rollback de @DataJpaTest).

Qué está evaluando la pregunta

Es una clase base abstracta de prueba donde se define el contenedor estático una sola vez. Todas las clases de test heredan de esa clase base, garantizando que el contenedor Docker se levanta solo una vez para toda la suite de pruebas del proyecto.

Reto del tutor

Un candidato dice: "Para no depender de Docker, mis tests de integración usan H2 con dialecto PostgreSQLDialect". Evalúa esta afirmación técnica.

Lo que una respuesta suele tener bien

Reconoce que H2 tiene una opción de emulación de dialecto PostgreSQL.

Lo que suele quedarse en la superficie

Decir que "es mejor Docker porque es más moderno".

El concepto de fondo que suele faltar

No entender que el modo de compatibilidad de H2 solo emula la sintaxis básica, pero no soporta tipos nativos como JSONB, UUIDs nativos, extensiones PostGIS, bloqueos transaccionales pesados ni secuencias idénticas a PostgreSQL real.

Cómo lo respondería alguien senior en una entrevista

Es un compromiso técnico obsoleto: la emulación de H2 da una falsa sensación de seguridad. Funciona para CRUDs triviales, pero fallará estrepitosamente en cuanto uses una función nativa de PostgreSQL (e.g. jsonb_extract_path, índices GIN, o consultas nativas). En la industria actual, el estándar indiscutible para pruebas de persistencia fiables es Testcontainers corriendo el motor PostgreSQL exacto de producción.

Proyecto «Pedidos» · paso 20 de 30

Tests de integración con PostgreSQL real

Probar repositorios y transacciones contra la misma base de datos que en producción.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Configura Testcontainers con PostgreSQL y @ServiceConnection.
  2. Prueba con @DataJpaTest el listado paginado y la consulta sin N+1.
  3. Prueba el caso de uso completo de confirmar un pedido con @SpringBootTest.

Está listo cuando

  • Los tests de integración pasan en un ordenador con Docker y sin configuración manual.
  • Un mismo contenedor se reutiliza en toda la suite.

Katas: ejercicios cortos

Reglas de oro

  • No uses H2 para probar código SQL de producción; usa Testcontainers.
  • Declara siempre los contenedores como static para no penalizar el tiempo de ejecución.
  • Usa @ServiceConnection para vincular automáticamente el contenedor con Spring Boot.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown