1. Significado de la elección de la fuente de datos
Si alguna vez has escrito una prueba de datos y pensaste «ya voy a meter datos rápido, con tal de que el repositorio devuelva algo», felicidades: ya te has topado con el problema más frecuente de @DataJpaTest: la prueba aparentemente verifica la consulta, pero en realidad verifica tu talento para inventar fixtures aleatorios. La elección de la fuente de datos es la manera de hacer la prueba predecible y legible, y no simplemente «verde».
En las pruebas de la capa de datos, en algún momento llega el dolor adulto: te das cuenta de que los datos son parte del escenario, no el fondo. Y si el fondo se crea de manera diferente cada vez, entonces el repositorio puede «funcionar» o «no funcionar» no por la consulta, sino porque accidentalmente creaste dos artículos con el mismo publishedAt, u olvidaste establecer el estado PUBLISHED, o creaste una categoría con el código "Java" en lugar de "java".
Desde un punto de vista práctico, la elección de la fuente de datos de prueba se trata de dos propiedades: primero, la prueba debe leerse de arriba abajo como una pequeña historia «qué estado tenía la BD → qué hicimos → qué verificamos». Segundo, la prueba debe ser resistente a la aleatoriedad: si ejecutas el suite 100 veces, debe comportarse igual 100 veces, y no «por inspiración de Hibernate».
Para no convertir la preparación de datos en magia, es útil tener en mente un esquema simple.
flowchart TD
A[Esquema BD] -->|Flyway| B[BD lista]
B --> C{¿Qué datos necesita la prueba?}
C -->|1-2 objetos, importante legibilidad en Java| D[Java setup: TestEntityManager]
C -->|importan cadenas exactas, id, orden, esquinas| E["@Sql: statements/scripts"]
C -->|se necesita fondo común para muchas pruebas| F[Migración solo de prueba]
D --> G[Act: método del repositorio]
E --> G
F --> G
G --> H[Assert]
Este diagrama no trata sobre «qué herramienta está más de moda». Se trata de que cada herramienta tiene un rol, y si se confunde el rol, las pruebas empiezan a pelearse entre sí, y tú, con tu propio repositorio.
2. Fuentes de datos: baseline y escenario
Cuando hablamos de «datos de prueba», muchos se imaginan un gran saco de datos. En la práctica, es más útil dividirlos mentalmente en dos capas: el baseline común (fondo que necesitan muchas pruebas) y el escenario local (lo que necesita una prueba concreta). Este truco reduce sorprendentemente el caos: dejas de intentar meter todo en un único data.sql universal, que luego incluso los desarrolladores más valientes temen tocar.
Si distribuimos estos roles por herramientas, el panorama es el siguiente. Las migraciones solo de prueba suelen funcionar como baseline: pequeños directorios estables que se necesitan a menudo, deben estar siempre presentes y ser deterministas. @Sql suele ser bueno como escenario local: para una prueba (o un pequeño conjunto de pruebas) cargas exactamente el conjunto de filas necesario para la verificación. Java-setup a través de TestEntityManager es conveniente cuando el escenario es pequeño y es más fácil describirlo con objetos que con SQL.
Es útil fijar «a grandes rasgos» lo que cada herramienta te promete como autor de la prueba:
| Fuente de datos | Qué ofrece | Dónde suele ser más adecuado |
|---|---|---|
| Java-setup (TestEntityManager) | Legibilidad a nivel Java, el compilador señala errores, es fácil crear un grafo de entidades | 1–3 entidades, importante la relación entre objetos, se desea ver el dominio en el código |
| @Sql (statements / scripts) | Cadenas exactas, ids exactos, esquinas exactas, sin «magia» de generación | escenarios donde son importantes valores fijos, orden, casos límite |
| Migraciones Flyway solo de prueba | Fondo común estable «como el aire», igual para todas las pruebas | directorios y datos de referencia estables (categorías, etc.) |
Y ahora la regla principal del día, que suena aburrida pero te ahorra semanas de vida: un conjunto de datos, una única fuente de verdad. Si tus categorías son «a veces migración, a veces script SQL, a veces helper Java», tarde o temprano obtendrás una prueba que falla porque «en esta rama hay 3 categorías, y en la otra 4», y estarás 40 minutos convenciéndote de que el problema es JPA (no, el problema es tu zoológico de fuentes).
3. Java-setup mediante TestEntityManager
Java-setup suele estar infravalorado porque parece: «bueno, es largo, en SQL es más rápido». Pero luego resulta que en SQL insertaste accidentalmente el category_id incorrecto, olvidaste un campo obligatorio o empezaste a tener en mente un id numérico que solo era estable hasta el primer registro extra. Java-setup es bueno porque te mantiene en el mundo de los tipos y el compilador: el IDE te sugiere los campos, te equivocas menos en los nombres y ves más fácilmente el propio dominio, no solo las tablas.
Para @DataJpaTest, la herramienta estándar aquí es TestEntityManager. Es especialmente bueno cuando necesitas levantar 1–3 entidades, mostrar un pequeño grafo de relaciones y hacer persistAndFlush() de inmediato. En tal escenario, la prueba se lee en el lenguaje del modelo: aquí está la categoría, aquí el artículo, aquí la relación entre ellos.
Pero Java-setup empieza rápidamente a perder cuando ya no luchas contra el escenario, sino contra la infraestructura: necesitas id fijos, timestamp exactos, orden determinista de filas, secuencias y una larga lista de campos obligatorios «solo para que la prueba se ejecute». Esa es una buena señal para cambiar a @Sql.
Y otra regla: es mejor no extender el baseline compartido a través de helpers Java. Es más cómodo mantener el fondo común en migraciones solo de prueba, y desde la prueba tomarlo por clave natural como code, en lugar de esperar que la categoría tenga siempre el mismo id numérico.
4. @Sql: configuración SQL de escenario
Hay situaciones donde SQL no es un «mal de bajo nivel», sino la forma más honesta de decirle la verdad a la prueba. Cuando la propia fila en la tabla es parte del escenario, @Sql suele ganar. Un id fijo, un slug concreto, un published_at exacto, un orden conocido de datos, una combinación límite de estados: todo esto se ve en SQL de inmediato y sin discusiones con generadores. Aquí es perfectamente normal fijar id = 10: este conjunto de datos pertenece a una sola prueba y no se hace pasar por un baseline común.
Si hay muy pocos datos, los statements inline son perfectamente adecuados. Cuando el escenario es más largo que un par de comandos, es más honesto sacarlo a scripts: el archivo recibe un nombre, formato y se lee como una pequeña historia del estado de la BD.
Pero la fuerza de @Sql radica precisamente en la localidad. Tal configuración debe responder a la pregunta «qué estado necesita esta prueba», no «qué más meter en la base por si acaso». Tan pronto como un script empieza a vivir como el aire común para la mitad del suite, ya no es un escenario, sino un candidato a migración baseline.
5. Migraciones Flyway solo de prueba
A veces los datos no los necesita una sola prueba, sino muchas. Entonces @Sql y Java-setup empiezan a duplicarse, y el suite se convierte gradualmente en una serie «volvamos a insertar la categoría java». En tal situación, las migraciones solo de prueba son la fuente de verdad más sensata: el esquema viene de las main migrations, el fondo de referencia común viene de las test-only migrations, y cualquier @DataJpaTest arranca desde el mismo estado.
Para ContentHub, el candidato clásico para tal baseline es Category. Es un directorio en el que es conveniente apoyarse en diferentes pruebas. Y aquí es más fiable aferrarse a la clave natural: si una prueba necesita la categoría java, es mejor buscarla por code — por ejemplo, mediante findByCode("java") — que coser a todo el suite una regla implícita id = 1. Un id sustituto puede desplazarse, mientras que la clave natural hace explícita la dependencia.
Lo principal es no confundir el baseline con un conjunto de datos oculto. Tan pronto como en el «fondo común» empiezan a vivir artículos publicados, archivos adjuntos y partes del flujo de trabajo, ya no es un baseline, sino un escenario local que debe devolverse a @Sql o Java-setup.
6. Cómo mezclar enfoques sin hacer un batiburrillo
En un proyecto real casi nunca ocurre «solo Java» o «solo SQL». La estrategia normal es combinada: el baseline reside en migraciones solo de prueba, y los escenarios locales se crean ya sea con Java-setup o con @Sql. Pero es importante no mezclar «por humor», sino mezclar «por rol». Tan pronto como mezclas enfoques sin roles, obtienes pruebas que no se leen: una parte de los datos «vino de algún lado», otra parte «se insertó en alguna parte», y ya no entiendes qué es lo que realmente se garantiza.
Una buena legibilidad de una prueba de datos a menudo se parece a la misma estructura: primero te apoyas en el fondo base (por ejemplo, la categoría java ya existe), luego agregas el escenario local (por ejemplo, creas un artículo), luego llamas al repositorio y haces la aserción.
Ejemplo de una prueba «mixta pero legible»: la categoría viene del baseline, y el artículo se crea localmente mediante Java-setup, porque nos importa la relación Article → Category precisamente como objeto.
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
class MixedSetupDataJpaTest {
@Autowired
TestEntityManager em;
@Autowired
CategoryRepository categoryRepository;
@Test
void shouldPersistDraftInReferenceCategory() {
// Arrange: baseline vive en migraciones, por lo tanto tomamos la categoría por clave natural
Category category = categoryRepository.findByCode("java").orElseThrow();
// Arrange: creamos el escenario local en Java, porque nos importa la relación de entidades
Article article = new Article("Intro", "intro", ArticleStatus.DRAFT, category);
// Act: guardamos y confirmamos en la BD, para que el repositorio trabaje con el estado real
em.persistAndFlush(article);
// Assert: verificamos que el artículo realmente se ha guardado
assertThat(article.getId()).isNotNull();
}
}
Observa que la categoría baseline aquí no está cosida a id=1: tomamos el fondo común por code, y creamos el artículo local donde vive el escenario.
Si en cambio necesitas «exactamente ese conjunto de filas» (por ejemplo, tres artículos publicados con slug y publishedAt predefinidos), Java-setup puede volverse demasiado verboso, y entonces es mejor hacer el escenario local con @Sql. Pero el baseline no debe duplicar esos artículos: el baseline sigue siendo un directorio, y el escenario, un escenario.
Otro punto importante de la mezcla es el orden de percepción. Si los datos provienen de migraciones baseline, la prueba no debe confiar en ellos «en silencio». Una buena prueba al menos una vez lee el valor baseline y hace visible la dependencia. Es como un buen contrato: no espera que «todos ya lo entiendan», sino que fija lo importante explícitamente.
7. Chuleta de elección
Cuando estás frente a una prueba, el cerebro suele querer una respuesta simple: «¿qué elijo?» Pero la respuesta correcta no está en la herramienta, sino en la pregunta “¿qué estado de la BD necesita esta prueba?”. Para no convertir la elección en filosofía, es útil usar una pequeña chuleta: no es un dogma, pero para pruebas de datos comunes funciona sorprendentemente bien.
Empieza por lo más práctico: ¿cuántos datos y qué tan «especiales» son? Si son 1–2 entidades, y solo se necesitan para que el método del repositorio no funcione en el vacío, Java-setup es casi siempre la mejor opción. Si los datos son «especiales» y quieres ids/slug/tiempos/orden fijos, @Sql suele ser más honesto. Si los datos los necesitan muchas pruebas y son un directorio, migración solo de prueba. Y esta regla no desaparece cuando la prueba se vuelve más amplia y costosa: cambia el costo de ejecución, pero no el desacople entre esquema, baseline común y escenario local.
Aquí hay una tabla compacta de decisiones:
| Pregunta para la prueba | Si la respuesta es «sí» | Normalmente elegimos |
|---|---|---|
| ¿Solo se necesitan un par de entidades, importa la legibilidad a nivel Java? | Sí | Java-setup (TestEntityManager) |
| ¿Son importantes los valores fijos (id/slug/timestamp), y quieres ver las filas explícitamente? | Sí | @Sql (scripts/statements) |
| ¿Estos datos los necesitan muchas pruebas como un directorio inmutable? | Sí | Migración Flyway solo de prueba |
| ¿El escenario debe ser local y comprensible directamente en la prueba? | Sí | Java-setup o @Sql (no migración) |
| ¿Te sorprendes a ti mismo con que «los mismos datos» están definidos en tres lugares? | Sí, me sorprendo | Dejar urgentemente una única fuente de verdad |
Y ahora el pequeño «árbol» de preguntas que puedes tener en mente:
flowchart TD
Q1[¿Los datos los necesitan muchas pruebas?] -->|Sí| Q2[¿Es un directorio, casi no cambia?]
Q2 -->|Sí| MIG[Migración solo de prueba]
Q2 -->|No| SQL["@Sql, pero cuidadosamente y local"]
Q1 -->|No| Q3[¿Se necesita un valor exacto de columna?]
Q3 -->|Sí| SQL
Q3 -->|No| JAVA[Java setup mediante TestEntityManager]
Observa que en el árbol no hay opción «siempre @Sql» o «siempre Java». Porque «siempre» es normalmente el camino hacia pruebas que son demasiado largas o demasiado frágiles.
8. Errores típicos al elegir la fuente de datos
Error nº1: mezclar los tres enfoques sin roles.
El panorama más común en un proyecto que «creció rápido»: en una prueba las categorías se crean mediante Java-setup, en otra se cargan con @Sql, y en una tercera aparecen de repente desde una migración solo de prueba. Como resultado, no estás seguro de qué datos están garantizados siempre y cuáles son locales. Esto se cura no con una nueva abstracción, sino con una solución simple: el directorio vive en un solo lugar (normalmente la migración), el escenario vive localmente (Java o @Sql), y la prueba hace visible su dependencia. Para datos de referencia compartidos, esto normalmente significa una búsqueda por clave natural, no una apuesta silenciosa por id=1.
Error nº2: duplicar el mismo conjunto de datos en diferentes fuentes.
Si la categoría java está tanto en la migración baseline como en base-categories.sql y también en el helper TestCategories, tarde o temprano alguien corregirá el nombre de la categoría en un lugar y olvidará en el otro. Las pruebas empezarán a fallar «extrañamente», porque diferentes pruebas viven en diferentes versiones de la realidad. Aquí ayuda la regla: un tipo de datos, una única fuente de verdad. Incluso si tienes muchas ganas de duplicar «por si acaso», mejor no lo hagas.
Error nº3: convertir las migraciones solo de prueba en un «vertedero de todo».
Las migraciones para pruebas son excelentes mientras contengan un baseline pequeño y comprensible. Pero si empiezan a volcar datos para cada nueva prueba («bueno, es conveniente, siempre estará ahí»), el baseline deja de ser baseline y se convierte en una dependencia oculta. Al cabo de un mes tienes una prueba que falla porque alguien modificó «los datos comunes» sin saber que tu prueba depende de ellos. Una buena migración para pruebas se parece a un directorio: corta, estable y aburrida (y aburrido en pruebas es un cumplido).
Error nº4: usar @Sql como carga universal «todo para todos».
Un gran archivo SQL que «sirve para todas las pruebas» suena como un ahorro de tiempo. En la práctica se convierte en un monolito imposible de mantener: cualquier cambio rompe decenas de pruebas, y temes eliminar una «fila extra» porque no sabes quién la necesita. @Sql es más fuerte precisamente en la escenificación: un archivo pequeño, nombre claro, una sola historia.
Error nº5: Java-setup demasiado largo que oculta el significado de la prueba.
Java-setup es genial mientras sea corto. Pero tan pronto como la mitad de la prueba consiste en rellenar campos obligatorios de la entidad, el significado de la verificación empieza a ahogarse en boilerplate. En ese momento es muy fácil equivocarse y crear datos diferentes a los que crees. Si ves que la preparación en Java se ha vuelto más larga que la verificación, es un motivo para parar y pensar: quizás es mejor @Sql (donde estableces explícitamente solo las columnas necesarias), o quizás necesitas simplificar tu escenario.
GO TO FULL VERSION