1. Transacciones en pruebas de datos
Si antes solo escribías pruebas para controladores o servicios, las transacciones podían sonar como algo del mundo de proyectos «adultos» donde todos visten traje y discuten sobre READ_COMMITTED. En @DataJpaTest la transacción aparece no por pompa, sino para hacer las pruebas predecibles: cada prueba obtiene un pequeño arenero en la base de datos y no tiene que limpiar después.
La transacción en sentido práctico es el modo «todo o nada». Dentro de ella realizas varias operaciones (insert/update/delete), y al final la base de datos confirma los cambios (commit) o los deshace (rollback), como si hubieras pulsado un potentísimo Ctrl+Z. Para las pruebas esto suena ideal: queremos crear datos para un escenario con confianza, verificar el comportamiento del repositorio y al final no dejar basura en la base de datos que rompa la siguiente prueba.
En una aplicación normal, el límite de la transacción suele vivir a nivel de la capa de servicio: el servicio abre una transacción, llama a los repositorios, todo termina — la transacción se cierra. En las pruebas de data-slice, Spring hace algo similar, solo que «envuelve» en una transacción la ejecución del método de prueba. Es decir, la prueba se convierte en un mini-escenario de trabajo con la base de datos, pero sin consecuencias para las pruebas vecinas.
2. Rollback por defecto en @DataJpaTest
Cuando oyes «rollback por defecto», es fácil imaginarse que la prueba «no escribe en la base de datos», y que todo ocurre en una simulación. No es así: las consultas SQL se ejecutan, las entidades se guardan y leen realmente, solo que el estado final se revierte al final de la prueba. Si no fuera así, las pruebas de repositorio no tendrían sentido — queremos ver el comportamiento del almacenamiento, no una representación teatral con mocks.
En @DataJpaTest, el ciclo de vida típico de un método de prueba se ve así: Spring Test inicia una transacción antes de ejecutar el método @Test, luego dentro de la pruebas ejecutas save, findById, count o cualquier otra operación del repositorio, y después de que el método termina, Spring hace rollback. La siguiente prueba comienza en una nueva transacción y no ve tus «artes» de la prueba anterior.
Para visualizarlo, es útil tener en mente un esquema simple:
flowchart TD
%% Cada vértice es un paso lógico de un método de prueba en @DataJpaTest
A["Inicio del método de prueba @Test"] --> B["Spring abre una transacción"]
B --> C["Arrange: preparamos datos (save/persist)"]
C --> D["Act: llamamos al método del repositorio"]
D --> E["Assert: verificamos el resultado"]
E --> F["Spring hace ROLLBACK"]
F --> G["Fin del método de prueba, base de datos en estado inicial"]
Y aquí aparece una expectativa importante: dentro de una misma pruebas ves todos tus cambios como si ya estuvieran «en la base de datos». Esto es normal, porque trabajas dentro de una misma transacción. Pero al finalizar la prueba, los cambios desaparecen. Si alguna vez escribiste scripts de migración y soñaste con poder deshacer errores con solo pulsar un botón, felicidades: en las pruebas funciona casi así.
3. Aislamiento de pruebas: cada @Test — una historia independiente
Las pruebas que dependen unas de otras suelen funcionar hasta que las ejecutas en CI, en otra máquina o simplemente en otro orden. Luego llega el clásico: «a mí me da verde, a ti rojo, vamos a tocar el orden de las pruebas». Rollback por defecto es una protección incorporada contra convertir el suite de pruebas en una telenovela.
La idea es simple: cada método de prueba debe ser autosuficiente. Él mismo prepara los datos, llama a lo que verifica y afirma el resultado. No debe esperar que «antes ya se creó un artículo» o que «después alguien limpiará la tabla». En @DataJpaTest, Spring nos ayuda a cumplir esta regla porque después de cada prueba hace un rollback de la transacción.
Un pequeño ejemplo con el repositorio de artículos ContentHub. Observa: aquí hay dos pruebas en una misma clase, pero no están relacionadas. Cualquiera que se ejecute primero debe ser correcta.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.*;
@DataJpaTest // Levantamos solo el slice JPA y envolvemos automáticamente cada prueba en una transacción
class ArticleRepositoryRollbackDataJpaTest {
@Autowired // Spring inyectará el repositorio real del contexto de la prueba data-slice
private ArticleRepository articleRepository;
@Test
void savesArticle_insideTestItExists() {
// Arrange: guardamos un artículo dentro de la transacción de prueba
articleRepository.save(TestArticles.draft("spring-data-basics"));
// Assert: dentro de esta misma transacción el registro es visible, por lo tanto count() == 1
assertThat(articleRepository.count()).isEqualTo(1);
// Importante: al finalizar la prueba Spring hará rollback, y los datos no «pasarán» a la siguiente prueba
}
}
Y la segunda prueba (en la misma clase) puede esperar honestamente que la tabla de artículos esté vacía (si no cargas datos iniciales en migraciones). No «espera nada de la prueba anterior», porque la prueba anterior ya fue revertida.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.Assertions.*;
class ArticleRepositoryRollbackDataJpaTest {
@Autowired
private ArticleRepository articleRepository;
@Test
void nextTest_startsCleanBecauseRollback() {
// Assert: si no tienes datos iniciales, después del rollback de la prueba anterior la tabla debe estar vacía
assertThat(articleRepository.count()).isZero();
}
}
Si sientes la tentación de hacer las pruebas «más rápidas» haciendo que una prueba prepare datos y otra los aproveche, parece un ahorro solo a nivel de un par de líneas. A nivel de robustez del suite, suele ser un desastre: pagas con dependencias entre pruebas, diagnóstico complejo y fallos raros pero muy molestos.
4. Rollback y Arrange–Act–Assert
Cuando una prueba vive dentro de una transacción, aparece una característica agradable: puedes en un mismo escenario preparar datos y verificarlos a través del repositorio, sin temor a «contaminar» la base de datos para otras pruebas. Pero esta comodidad a veces provoca otro problema: la gente empieza a meter en una sola prueba todo lo que se le ocurre, porque «la base de datos se va a revertir de todos modos». Se revertirá, pero leer esa prueba será doloroso.
El enfoque más saludable es seguir pensando en términos de Arrange–Act–Assert. Simplemente, en las pruebas de repositorio el Arrange suele ser la creación de varias entidades (artículo, categoría, autor), Act — llamar al método del repositorio, Assert — verificar el resultado. Es importante que los Assert demuestren precisamente la promesa de tu escenario, no «todo lo que está bien en el mundo».
Ejemplo de un escenario AAA pequeño y «honesto»: queremos demostrar que la búsqueda por slug funciona.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class ArticleRepositoryRollbackDataJpaTest {
@Autowired
private ArticleRepository articleRepository;
@Test
void findBySlug_returnsSavedArticle() {
// Arrange: creamos y guardamos la entidad necesaria para el escenario
articleRepository.save(TestArticles.draft("spring-data-basics"));
// Act: ejecutamos la acción objetivo (llamada al método del repositorio)
Optional<Article> found = articleRepository.findBySlug("spring-data-basics");
// Assert: verificamos solo lo que promete el escenario (encontrado/no encontrado)
assertThat(found).isPresent();
}
}
Observa que esta prueba no intenta demostrar al mismo tiempo «que el estado es DRAFT, que el autor es alice, que createdAt se rellenó y que las migraciones se aplicaron». Todo eso puede ser importante, pero una prueba, un acuerdo. Si no mantienes este principio, el rollback no salvará de la degradación de la legibilidad: las pruebas serán verdes, pero inútilmente gruesas.
A menudo ayuda escribir pequeños comentarios en la prueba (sí, este es ese raro caso en que los comentarios en las pruebas no son malos, sino que ayudan al lector):
Article saved = articleRepository.save(TestArticles.draft("spring-data-basics")); // Arrange: preparamos datos
Optional<Article> found = articleRepository.findBySlug(saved.getSlug()); // Act: ejecutamos la acción
assertThat(found).isPresent(); // Assert: verificamos el resultado
5. Limpieza manual: cuándo sobra
El primer reflejo de muchos desarrolladores, especialmente después de experiencia con pruebas de integración «manuales», es: «después de la prueba hay que limpiar la base de datos». En @DataJpaTest este reflejo casi siempre es innecesario. Rollback por defecto hace la limpieza automáticamente, y generalmente mucho más rápido y fiable que un deleteAll() hecho a mano.
Cuando añades limpieza manual, creas ruido adicional en las pruebas y, lo que es peor, carga extra en la base de datos. Como resultado, las pruebas pueden volverse más lentas, y además empiezas a creer que las pruebas están aisladas «porque limpiamos», cuando en realidad el aislamiento ya está garantizado por las transacciones.
Aquí hay un ejemplo de «limpieza innecesaria» que se ve bien solo hasta que la multiplicas por 200 pruebas:
import org.junit.jupiter.api.AfterEach;
import org.springframework.beans.factory.annotation.Autowired;
class ArticleRepositoryRollbackDataJpaTest {
@Autowired
private ArticleRepository articleRepository;
@AfterEach
void cleanup() {
// Normalmente esto sobra: @DataJpaTest hará rollback automático después de cada @Test
// Y deleteAll() crea ruido adicional y consultas extra a la base de datos
articleRepository.deleteAll();
}
}
En la mayoría de los casos, es mejor confiar en el comportamiento por defecto de @DataJpaTest: una prueba, una transacción, un rollback. Si aun así añades limpieza manual, al menos debes entender honestamente por qué. Por ejemplo, has desactivado conscientemente el rollback (más abajo se explica) o estás creando datos fuera de la transacción (algo poco común en las pruebas data-slice, pero teóricamente posible).
6. Límites del rollback
El rollback suena a magia, pero la magia, como en Java, termina donde sales de los límites del contrato. La transacción revierte los cambios en la base de datos dentro de esa transacción, pero no «revierte todo el universo». Si esperas que el rollback devuelva al estado inicial absolutamente todo, puedes tener fallos extraños y molestos rápidamente.
Es útil tener en mente una tabla breve. No es para memorizarla, sino para que el cerebro no construya suposiciones falsas.
| Qué haces en la prueba | ¿Se revierte con rollback? | Por qué |
|---|---|---|
| save() de un artículo a través del repositorio (insert/update) | Sí | Los cambios en la BD están en la transacción de la prueba |
| delete() a través del repositorio | Sí | También es DML dentro de la transacción |
| Lectura de datos (findById, findBySlug) | No se «revierte», pero tampoco cambia la BD | La lectura por sí misma no deja rastro |
| Datos cargados al iniciar el contexto (migraciones/datos iniciales) | No | Aparecieron antes de la transacción del método de prueba |
| Cambios en secuencias/generadores de id | Puede que no sea como esperas | En algunos SGBD la generación de id puede tener vida propia |
| Escritura de un archivo en disco (si hicieras I/O) | No | El sistema de archivos no participa en la transacción de la BD |
La «sorpresa» más común es intentar depender de un valor concreto de id, por ejemplo esperar que el primer artículo en la prueba siempre obtenga id = 1. En algunas bases de datos coincide por casualidad, en otras no, y en otras dependerá de cómo esté configurada la generación de identificadores. Por lo tanto, la regla es simple: en las pruebas de repositorio casi siempre se verifica que id no sea null (o que sea positivo), no que sea igual a un número concreto.
Aquí hay un ejemplo de mala idea que puede hacer la prueba frágil:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class ArticleRepositoryRollbackDataJpaTest {
@Test
void doNotAssertExactIds() {
// articleRepository aquí se entiende como un repositorio inyectado por Spring (como en ejemplos anteriores)
Article saved = articleRepository.save(TestArticles.draft("spring-data-basics"));
// Assert: frágil y a menudo sin sentido — el id puede depender del SGBD/estrategia de generación/orden de ejecución
assertThat(saved.getId()).isEqualTo(1L);
}
}
Mucho más saludable:
assertThat(saved.getId()).isNotNull();
El rollback tampoco significa que «dentro de la prueba la base de datos no existe». La base de datos existe, JPA existe, el repositorio existe, y precisamente por eso @DataJpaTest detecta problemas de mapeo y consultas. Simplemente después de la prueba todo vuelve al estado inicial, para que la siguiente prueba no viva sobre las ruinas de la anterior.
7. Desactivar el rollback: precio y consecuencias
A veces surge el deseo de «hacer commit de los cambios» en una prueba. Normalmente este deseo nace no de una necesidad, sino del cansancio: quieres preparar los datos una vez y luego leerlos en otras pruebas. Pero esto es como mantener la casa limpia metiendo todo debajo del sofá: al principio parece cómodo, pero luego da miedo asomarse.
Técnicamente, Spring Test permite desactivar el rollback y forzar que la transacción haga commit. Generalmente se usa @Rollback(false) (o mecanismos equivalentes). Pero es importante entender el precio: pierdes inmediatamente el aislamiento automático y tendrás que limpiar la base de datos manualmente o vivir con pruebas que se afectan entre sí.
Mini-ejemplo de cómo se ve (lo muestro como mecanismo, no como recomendación para uso diario):
import org.junit.jupiter.api.Test;
import org.springframework.test.annotation.Rollback;
class ArticleRepositoryRollbackDataJpaTest {
@Test
@Rollback(false) // Desactivamos el rollback: los cambios se confirmarán y «pasarán» a otras pruebas
void commitsChanges_notRecommendedByDefault() {
// Arrange/Act: escribimos datos en la BD de forma «real»
articleRepository.save(TestArticles.draft("spring-data-basics"));
// Aquí normalmente no se hace Assert: el ejemplo muestra solo el mecanismo (y su precio), no un buen caso de prueba
}
}
En la mayoría de los casos, para las pruebas de la capa de repositorio de ContentHub esto no es necesario. Si la prueba necesita datos, debe crearlos ella misma en la parte de Arrange. Esto aumenta la localidad del escenario y facilita el diagnóstico: la prueba falló — la abres y ves inmediatamente todas las precondiciones, no tienes que buscar «quién hizo commit antes».
8. Errores típicos con @DataJpaTest
Error nº1: hacer una prueba «preparatoria» y otra «verificadora».
Parece una pequeña optimización: «en una prueba creo un artículo, y en otra verifico findBySlug». Pero debido al rollback por defecto, descubrirás de repente que la prueba «verificadora» no encuentra datos. Y esto es exactamente el comportamiento correcto: las pruebas no deben estar vinculadas. Si necesitas un estado, prepáralo en la misma prueba para que el escenario sea autosuficiente.
Error nº2: esperar que el rollback haga la prueba «no real» y por lo tanto no confiar en los resultados.
A veces los estudiantes piensan: «Como hay rollback, la base de datos no participa, así que esto no es serio». En realidad, @DataJpaTest trabaja con la capa de persistencia real: se levanta JPA, se crea el esquema, se ejecutan consultas. El rollback es necesario solo para la limpieza del entorno después de la prueba, no para la simulación.
Error nº3: depender de valores concretos de id y otros detalles «casualmente estables».
Cuando la prueba escribe assertThat(saved.getId()).isEqualTo(1L), convierte el comportamiento del generador de identificadores en parte del contrato del dominio. Normalmente no es un contrato. Por eso las pruebas se rompen con cambios menores en la configuración de la base de datos o en la estrategia de generación de id, aunque el riesgo funcional real no haya cambiado.
Error nº4: añadir deleteAll() «por si acaso» y considerarlo normal.
El rollback ya aísla las pruebas. La limpieza manual a menudo solo ralentiza el suite, añade ruido y puede enmascarar efectos extraños: no te das cuenta de que hay un insert/delete de más porque «limpias todo». En las pruebas data-slice, es mejor seguir una regla simple: preparamos datos en Arrange, verificamos, confiamos en el rollback.
Error nº5: mezclar en una misma prueba varias historias independientes, justificándose con que «total, se revertirá».
El rollback ciertamente revertirá, pero no devolverá la claridad al lector de tu prueba. Si en un mismo método creas un artículo, lo envías a diferentes estados, haces tres búsquedas distintas y además verificas contadores, esa prueba se convierte en una novela que nadie leerá hasta el final. Una prueba de repositorio es buena cuando es corta, enfocada y demuestra un acuerdo claro y comprensible.
GO TO FULL VERSION