CodeGym /Cursos /Spring Test /Esquema de BD en pruebas de data-layer

Esquema de BD en pruebas de data-layer

Spring Test
Nivel 18, Lección 0
Disponible

1. Esquema de BD en pruebas de data-layer

Cuando escuchamos «prueba de repositorio», el cerebro a veces dibuja una imagen ingenua: "estoy probando el método del repositorio, luego solo pruebo código Java". Pero el data-layer es una capa que vive físicamente en la frontera de dos mundos: el mundo de objetos (Article, Category) y el mundo de tablas/columnas/restricciones. Y una prueba, incluso la más pequeña, inevitablemente pasa a través de esa frontera.

En las pruebas unitarias normales podríamos decir: «Bueno, si no gusta, lo mockeamos». En data-layer no funciona así: el repositorio es la "conversación con la base de datos". Si no hay base de datos en la prueba, o si el esquema de la base de datos no coincide con las expectativas, entonces el repositorio se convierte como un diplomático sin intérprete: parece que llegó a la reunión, pero no puede ponerse de acuerdo.

En el proyecto ContentHub esto es especialmente obvio, porque tenemos tablas (y entidades relacionadas) articles, categories, article_attachments, tenemos unicidad de slug, campos not-null, relaciones y claves foráneas. Cualquier prueba de repositorio se basa en que estas tablas realmente existen y están organizadas como pensamos. Y aquí ocurre un importante giro metodológico: el esquema de BD no es «infraestructura en algún lugar cercano», sino parte del contrato que estamos obligados a verificar con pruebas.

Esquema de BD en palabras sencillas

La palabra «esquema» a veces suena como algo del mundo de adultos: DBA, diagramas, aburrimiento, café sin azúcar. En realidad, el esquema es simplemente una descripción formalizada de cómo la base de datos almacena los datos. Si simplificamos al máximo, el esquema responde a la pregunta: «qué cajas tenemos, qué compartimentos tienen y cuáles son las reglas de almacenamiento».

Una tabla es una caja. Una columna es un compartimento en la caja. El tipo de columna es el tamaño y la forma del compartimento (en cuál se puede poner un Instant, y en cuál solo un número). Las restricciones (NOT NULL, UNIQUE, FOREIGN KEY) son las reglas del almacén: aquí no se puede poner una caja vacía, aquí no se pueden almacenar dos slugs iguales, y este registro no puede hacer referencia a una categoría que no existe.

¿Por qué es importante esto específicamente para las pruebas? Porque una entidad JPA no es el esquema. La entidad es nuestra «versión de la realidad» del lado de Java. Y el esquema es la realidad del lado de la BD. Y si estas dos realidades comienzan a divergir, las pruebas de datos se convierten en ese momento de la verdad cuando la aplicación deja de pretender que «todo está bien».

Prueba de repositorio como contrato entre Java y SQL

Es muy útil aprender a mirar la prueba de data-layer como una verificación del contrato entre tres cosas: su código Java (entities), su mapeo ORM (anotaciones @Entity, @Column, @ManyToOne) y el esquema SQL real (tablas, columnas, constraints). El repositorio se sienta encima y parece «solo una interfaz», pero de facto solo funciona si todo este triángulo coincide.

Para que esto no se sienta como filosofía, sino como realidad ingenieril, imaginemos el camino de cualquier @DataJpaTest de forma simplificada:

flowchart TD
    A["JUnit 6 ejecuta la prueba"] --> B["Se levanta el slice de Spring Data JPA"] 
    B --> C["Se crea DataSource y conexión a BD"]
    C --> D["El esquema de BD debe existir y coincidir con las expectativas"]
    D --> E["Hibernate/JPA mapea las entidades a tablas/columnas"]
    E --> F["El Repository ejecuta consultas / persist"]
    F --> G["Assertions: verificamos el resultado"]

Aquí el nodo clave es el punto sobre el esquema. Si el esquema no se levantó, o se levantó de manera incorrecta, todo lo que sigue puede ni siquiera comenzar. La prueba puede caer «en la entrada», antes de ejecutar su método @Test. Y esto es lo que a menudo sorprende a los novatos: «¿Por qué mi prueba ni siquiera empezó a ejecutarse?». La respuesta es simple: porque para una prueba de data-layer, el "inicio del entorno" ya es parte de la verificación.

Dos tipos de roturas: consulta y esquema

El hábito práctico más útil hoy es aprender a distinguir dos clases de problemas. Externamente ambos se ven como «la prueba falló», pero el diagnóstico y el tratamiento son diferentes. Una clase de problemas se refiere a que nuestro código Java/ORM accede incorrectamente a la base de datos. La segunda, a que la base de datos (su estructura) no corresponde a lo que pensamos de ella.

A continuación, una tabla que es útil tener en mente como un mini «rayos X» de las caídas de pruebas de datos:

Qué se rompió Dónde suele caer Cómo se siente Causa típica
Consulta / mapeo Durante la ejecución de persistAndFlush(), find...(), repository.save() o una consulta específica La prueba arrancó, llegó al método, y luego falló JPQL incorrecto, @JoinColumn equivocado, fetch inapropiado, conflicto de tipos, ordenamiento incorrecto
Esquema Al levantar el contexto de prueba o en el primer flush (cuando la BD comienza a verificar la realidad) “ApplicationContext failed to start” o “relation/column not found” Tablas/columnas/restricciones no creadas, orden de cambios incorrecto, nombre/tipo de columna no coincide

Observe el detalle: el error de esquema puede manifestarse «más tarde», en el flush. Por ejemplo, el contexto se levantó porque las tablas existen, pero una columna específica no existe — y lo verá solo cuando intente escribir algo allí por primera vez. Por eso usamos tan a menudo persistAndFlush en las pruebas de datos: obliga a la base de datos a «decir la verdad» antes de que tengamos tiempo de construir una falsa confianza.

Schema drift en ContentHub

Schema drift es una frase bonita que en la práctica significa una historia muy cotidiana: «cambiamos el código pero olvidamos cambiar la base de datos» o «cambiamos la base de datos pero olvidamos cambiar el código». Y sí, esto ocurre incluso en equipos fuertes, porque no es una cuestión de inteligencia, sino de disciplina y proceso.

Imaginemos un día típico de un desarrollador. Decidió que en Article necesita almacenar la fecha de publicación en la columna published_at. En Java agregó el campo publishedAt y, quizás, incluso anotó cuidadosamente @Column(name = "published_at"). Las pruebas compilan, el IDE está verde, el ánimo es bueno, la vida es maravillosa. Y luego aparece una nueva prueba de datos en el repositorio que guarda un artículo publicado… y la base de datos de repente dice: «¿qué published_at?».

En ContentHub el drift es especialmente traicionero, porque tenemos invariantes importantes en la base de datos. slug debe ser único. title, summary, body y category son obligatorios. Los adjuntos deben hacer referencia a un artículo. Si la migración no agregó la restricción unique, entonces las pruebas pueden «pasar accidentalmente» con datos pequeños, y luego en producción la base de datos aceptará dos slugs iguales — y obtendrá un bug del tipo «¿por qué se abre el artículo público equivocado?». Si por el contrario existe la restricción unique, pero en el código Java el slug se genera sin considerar colisiones, entonces la base de datos honestamente comenzará a fallar — y es bueno que eso suceda en las pruebas, no con los usuarios.

Schema drift no es un «problema interno de la BD». Es un defecto de producto del data-layer. Y las pruebas de datos no son solo para decir «el método del repositorio funciona», sino para decir «nuestro data-layer vive en armonía con cómo está organizada la base de datos».

Caída de la prueba al inicio como señal

Un principiante tiene una reacción natural: si la prueba falló antes de ejecutar el método @Test, entonces «JUnit no se inició» o «algo se rompió en el entorno». Quiere apagar algo rápidamente, poner un stub, desactivar migraciones, solo para llegar a las aserciones «reales». Pero en data-layer esa reacción es una trampa.

Piense en ello así: si el contexto del data-slice no puede levantarse debido al esquema, eso significa que en la vida real la aplicación tampoco podrá funcionar correctamente con la base de datos. Sí, el entorno de producción puede ser diferente, pero la idea es: si tablas/columnas/restricciones no coinciden con las expectativas, la capa de repositorio no tiene oportunidad de ser correcta.

Por lo tanto, la caída al inicio no es ruido. Es una prueba que dice: «ni siquiera he empezado a verificar su idea de negocio porque falta el fundamento». Es como intentar comprobar si uno se sienta cómodamente en una silla cuando la silla aún no está montada y está en un montón de piezas. Y no, no es un problema de la prueba. Es un problema de «la silla no está ensamblada».

En una cultura de pruebas madura, tales caídas se perciben como «rápidamente atrapamos un defecto crítico» — y es motivo de alegría, no de irritación. La prueba le ahorró tiempo, nervios y un par de publicaciones en el chat corporativo al estilo «¿a quién le falló? a mí me falló, ¿y por qué falló?».

2. Ejemplos en ContentHub

CategorySchemaTest y «la columna incorrecta»

Ahora tendremos dos ejemplos cortos que deliberadamente parecen casi ridículamente simples. Esto es intencional: queremos ver que incluso la prueba de datos más «plana» sobre una sola entidad depende en realidad del esquema de la BD. No de la lógica de negocio, no del controlador, sino de la tabla, las columnas y su corrección.

El primer ejemplo: verificamos que Category realmente pueda ser guardada. No es «estamos probando persist como framework» (no somos fanáticos de probar Spring Data por Spring Data), sino «estamos comprobando que existe la tabla de categorías y que es compatible con el mapeo».

import org.junit.jupiter.api.Test; 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

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

@DataJpaTest // Levantamos solo el slice JPA del contexto: DataSource, EntityManager, repositorios, etc.
class CategorySchemaTest {

    @Autowired
    private TestEntityManager em; // Acceso simplificado a persist/flush en pruebas

    @Test
    void shouldPersistCategoryWhenSchemaMatches() {
        Category category = new Category();
        // Es importante llenar los campos que en el esquema están marcados como obligatorios (NOT NULL)
        category.setCode("java");
        category.setName("Java");

        // persistAndFlush obliga a la base de datos a "decir la verdad" de inmediato: verifica tablas/columnas/restricciones
        em.persistAndFlush(category);

        // Si apareció id, significa que el insert se realizó y el esquema no entra en conflicto con el mapeo
        assertThat(category.getId()).isNotNull();
    }
}

Aquí lo importante no es que «id se volvió not null» (aunque también es útil). Lo importante es que la prueba pudo hacer persistAndFlush(). Ya demostró: la tabla existe, las columnas existen, los tipos de columnas satisfacen a la BD, las restricciones no contradicen los datos. En otras palabras, el esquema y el mapeo no se pelearon.

El segundo ejemplo es un fragmento microscópico de entity. Muestra el lugar donde el drift se manifiesta con más frecuencia: la discrepancia en el nombre de la columna.

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

import java.time.Instant;

@Entity // Esta entidad debe ser comparable con la tabla real en la BD
class Article {

    @Id
    private Long id; // Identificador: esperamos que la tabla tenga una clave primaria (o al menos una columna para ello)

    @Column(name = "published_at") // El nombre de la columna debe coincidir 1 a 1 con lo que realmente se creó en las migraciones
    private Instant publishedAt; // El tipo también es importante: por ejemplo, para Instant normalmente esperamos timestamptz/timestamp
}

Ahora imagine que en el esquema real la columna no se llama published_at, sino, digamos, publishedAt o published_time. Para la base de datos son nombres completamente diferentes. El resultado suele ser muy «amigable»: algo como «column not found» o «unknown column». Y no es un bug de la prueba. Es un bug de desincronización.

Para sentir lo que es «el esquema es la fuente de la verdad», es útil ver también un fragmento SQL (incluso si aún no le gusta SQL — no importa, se acostumbrará). Tal fragmento de esquema podría verse así:

create table articles (
  id bigserial primary key,          -- PK + autogeneración de id (aproximadamente lo que esperan muchos mapeos)
  published_at timestamptz            -- nombre y tipo deben coincidir con @Column y el tipo del campo
);

Si en el SQL no hay published_at, pero en la entidad sí (o viceversa), las pruebas de datos tarde o temprano lo manifestarán. Y esta es, de hecho, una de las principales ventajas del suite de data-layer: funciona como una alarma de «ya nos hemos desviado, y empieza a ser peligroso».

3. Errores típicos al verificar el esquema

Error nº1: considerar el esquema «no parte de la prueba» e intentar arreglar la caída solo a nivel de aserciones.
Cuando la prueba falla por una tabla o columna faltante, no tiene oportunidad de «pasar después de modificar assertThat». Es como cambiar la pegatina de «todo bien» en el panel de instrumentos cuando el motor humea. El hábito correcto aquí es siempre preguntarse: ¿la prueba falló en el momento de ejecución del método o ya al iniciar el contexto? Si es al inicio, muy a menudo es una historia sobre el esquema, no sobre su verificación.

Error nº2: habilitar «temporalmente» la autogeneración del esquema de Hibernate para que las pruebas pasen, y olvidarlo.
Esto es especialmente traicionero. Activa la creación automática de tablas a partir de las entidades, y las pruebas realmente comienzan a «ponerse verdes alegremente». Pero acaba de dejar de probar el esquema real que vive mediante migraciones. Como resultado, verifica un mundo abstracto de «cómo lo haría Hibernate», no el mundo real de «cómo está realmente organizada la base de datos». El suite de data-layer se convierte en buen humor sin evidencias.

Error nº3: confundir «se rompió el esquema» y «se rompió la consulta», y luego diagnosticar mal.
Si el problema está en el esquema, puede pasar horas leyendo la consulta JPQL buscando una coma extra, aunque la consulta nunca llegó a ejecutarse: la base de datos no se levantó o la tabla no existe. Y viceversa, si el esquema es correcto pero la consulta es incorrecta, se puede rehacer migraciones infinitamente, aunque el problema está en la lógica de selección. Una disciplina útil es entender primero en qué etapa falló: inicio del contexto, flush, ejecución de la consulta o aserción.

Error nº4: pensar que «las tablas están en algún lugar» y no hacer flush, porque «así también funciona».
Sin flush, corre el riesgo de verificar no la base de datos, sino el contexto de persistencia. Un objeto managed en memoria puede verse ideal, incluso si la base de datos al escribir hubiera fallado por una restricción. persistAndFlush suena un poco más aterrador, pero es más honesto: obliga a la base de datos a participar en la conversación de inmediato, no al final, cuando ya ha construido media prueba sobre arena.

Error nº5: tratar el schema drift como una nimiedad y posponer la corrección «para después».
El data-layer es el fundamento. Si aparece drift, cualquier prueba siguiente se vuelve menos predecible: una prueba falla «de manera extraña», otra «pasa accidentalmente», una tercera deja de ser reproducible. Es ese raro caso en que «después» a menudo se convierte en «nunca», y el costo crece como una suscripción de streaming cada año.

1
Tarea
Spring Test, nivel 18, lección 0
Bloqueada
Guardar `TrainingTopic` mediante `TestEntityManager`
Guardar `TrainingTopic` mediante `TestEntityManager`
1
Tarea
Spring Test, nivel 18, lección 0
Bloqueada
Lectura de `WarehouseCell` a través del repositorio después de `flush` y `clear`
Lectura de `WarehouseCell` a través del repositorio después de `flush` y `clear`
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION