
Qué carga @DataJpaTest
@DataJpaTest autoconfigura todo lo necesario para trabajar con JPA y nada más:
- Entities con
@Entity. - Repositorios
@Repositoryy los que extiendenJpaRepository,CrudRepository, etc. EntityManageryTestEntityManager.DataSource(sustituido por H2 in-memory por defecto).- Configuración de Hibernate y JPA.
- Logging SQL si está configurado en
application-test.yml.
Lo que no carga: controllers, servicios @Service, beans web, beans de Kafka, configuración de seguridad. Todo eso queda fuera porque no es relevante para probar persistencia.
graph TD
A[Test con @DataJpaTest] --> B[Entities @Entity]
A --> C[Repositories]
A --> D[EntityManager / TestEntityManager]
A --> E[DataSource H2 o Testcontainers]
A --> F[Hibernate config]
A -.NO carga.-> G[Controllers]
A -.NO carga.-> H[Services @Service]
A -.NO carga.-> I[Web beans / Security]
Cada test queda envuelto en una transacción que hace rollback al final. Eso significa que los tests no se contaminan entre sí: el repositorio "ve" lo que el test acaba de escribir, pero al terminar la BD vuelve a su estado inicial.
Primer test con TestEntityManager
TestEntityManager es la herramienta para preparar fixtures sin pasar por el repositorio (lo que mantiene los tests independientes del propio código que se está validando). Permite hacer persist, flush, clear y find sin escribir queries.
@Entity
@Table(name = "empleados")
@Data @NoArgsConstructor @AllArgsConstructor
public class Empleado {
@Id @GeneratedValue
private Long id;
private String nombre;
private String email;
private BigDecimal salario;
@ManyToOne private Departamento departamento;
}
public interface EmpleadoRepository extends JpaRepository<Empleado, Long> {
Optional<Empleado> findByEmail(String email);
List<Empleado> findBySalarioGreaterThan(BigDecimal salario);
}
@DataJpaTest
class EmpleadoRepositoryTest {
@Autowired
TestEntityManager em;
@Autowired
EmpleadoRepository repository;
@Test
void findByEmail_devuelveEmpleadoCuandoExiste() {
em.persist(new Empleado(null, "Alan", "alan@certidevs.com",
new BigDecimal("48000"), null));
em.flush();
em.clear();
var resultado = repository.findByEmail("alan@certidevs.com");
assertThat(resultado).isPresent();
assertThat(resultado.get().getNombre()).isEqualTo("Alan");
}
@Test
void findByEmail_devuelveVacioCuandoNoExiste() {
var resultado = repository.findByEmail("nadie@certidevs.com");
assertThat(resultado).isEmpty();
}
}
Tres detalles clave:
em.flush()fuerza el INSERT contra la BD; sin él, Hibernate retiene el cambio en memoria.em.clear()vacía elPersistenceContextpara que la siguiente lectura realmente vaya a BD y no devuelva el objeto cacheado.- El
assertThatviene de AssertJ, incluido por defecto con Spring Boot Starter Test.
Probar queries derivadas
Spring Data deriva queries del nombre del método (findByEmail, findBySalarioGreaterThan, etc.). El slice test es el lugar adecuado para verificar que esas derivaciones funcionan como esperamos:
@Test
void findBySalarioGreaterThan_devuelveSoloEmpleadosConSalarioMayor() {
em.persist(new Empleado(null, "Lucía", "lucia@certidevs.com",
new BigDecimal("38000"), null));
em.persist(new Empleado(null, "Carlos", "carlos@certidevs.com",
new BigDecimal("52000"), null));
em.persist(new Empleado(null, "Marta", "marta@certidevs.com",
new BigDecimal("60000"), null));
em.flush();
var resultado = repository.findBySalarioGreaterThan(new BigDecimal("45000"));
assertThat(resultado)
.hasSize(2)
.extracting(Empleado::getNombre)
.containsExactlyInAnyOrder("Carlos", "Marta");
}
El test no asume orden por defecto: containsExactlyInAnyOrder evita falsos positivos cuando la BD entrega resultados en otro orden.
Probar @Query y JPQL
Las queries con @Query merecen un test específico, especialmente cuando combinan joins o expresiones JPQL complejas:
public interface EmpleadoRepository extends JpaRepository<Empleado, Long> {
@Query("""
SELECT e FROM Empleado e
JOIN e.departamento d
WHERE d.ciudad = :ciudad
AND e.salario >= :minimo
ORDER BY e.salario DESC
""")
List<Empleado> buscarPorCiudadYSalarioMinimo(
@Param("ciudad") String ciudad,
@Param("minimo") BigDecimal minimo);
}
@Test
void buscarPorCiudadYSalarioMinimo_filtraYOrdena() {
var madrid = em.persist(new Departamento(null, "Ingeniería", "Madrid"));
var valencia = em.persist(new Departamento(null, "Ventas", "Valencia"));
em.persist(new Empleado(null, "Alan", "alan@certidevs.com",
new BigDecimal("48000"), madrid));
em.persist(new Empleado(null, "Lucía", "lucia@certidevs.com",
new BigDecimal("55000"), madrid));
em.persist(new Empleado(null, "Carlos", "carlos@certidevs.com",
new BigDecimal("60000"), valencia));
em.flush();
em.clear();
var resultado = repository.buscarPorCiudadYSalarioMinimo(
"Madrid", new BigDecimal("40000"));
assertThat(resultado)
.hasSize(2)
.extracting(Empleado::getNombre)
.containsExactly("Lucía", "Alan"); // ordenado por salario DESC
}
Si la query usa nativeQuery = true, este tipo de test se vuelve imprescindible porque el SQL nativo no es portable entre motores. Lo veremos en el siguiente bloque.
Testcontainers para PostgreSQL real
H2 in-memory es rápido pero miente. Soporta funciones que PostgreSQL no tiene, ignora limitaciones de JSON, no entiende LATERAL JOIN, ON CONFLICT, ni los tipos jsonb, tsvector, interval. Los tests pasan en local y se rompen en producción.
La solución es ejecutar los tests contra el motor real con Testcontainers:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class EmpleadoRepositoryPostgresTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:17-alpine");
@Autowired
TestEntityManager em;
@Autowired
EmpleadoRepository repository;
@Test
void busquedaConJsonb_devuelveResultadoEsperado() {
// Test que solo tiene sentido contra PostgreSQL real
}
}
Tres anotaciones que cambian todo:
@AutoConfigureTestDatabase(replace = NONE)desactiva la sustitución automática a H2. Spring Boot usa elDataSourceque se le proporcione.@ServiceConnection(Spring Boot 4) configura automáticamente la URL, usuario y contraseña del contenedor en el contexto. Sustituye el viejo@DynamicPropertySourcecon boilerplate.@Testcontainersy@Containerson del soporte JUnit 6 de Testcontainers.
Mantén Testcontainers en una clase base abstracta que reutilicen todos los tests
@DataJpaTest. Así el contenedor arranca una sola vez por sesión Maven o Gradle (gracias al patrónstatic), no por test.
Cargar fixtures con @Sql
Para datasets grandes o cuando el test inicial necesita un estado complejo, @Sql es más limpio que insertar registro a registro:
@Test
@Sql(scripts = "/sql/empleados-completos.sql")
void buscarPorEmail_conDatasetGrande() {
var resultado = repository.findByEmail("ana@certidevs.com");
assertThat(resultado).isPresent();
}
Con el archivo src/test/resources/sql/empleados-completos.sql:
INSERT INTO departamentos (id, nombre, ciudad) VALUES
(1, 'Ingeniería', 'Madrid'),
(2, 'Ventas', 'Valencia');
INSERT INTO empleados (id, nombre, email, salario, departamento_id) VALUES
(10, 'Ana', 'ana@certidevs.com', 50000, 1),
(11, 'Bruno', 'bruno@certidevs.com', 42000, 2);
@Sql se ejecuta antes del test. Para cargar datos solo en un grupo de tests, anótalo a nivel de clase. Para limpiar manualmente entre tests, usa executionPhase = AFTER_TEST_METHOD con un script de truncate.
Rollback automático y transacciones explícitas
Por defecto, @DataJpaTest envuelve cada test en una transacción que hace rollback al terminar. Esto asegura aislamiento y velocidad, pero tiene una consecuencia: si el código bajo test gestiona transacciones (commits, propagación REQUIRES_NEW), el test no reproduce la realidad.
Para tests que necesitan ver commits reales:
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class EmpleadoRepositoryConCommitsTest {
@Autowired
EmpleadoRepository repository;
@Test
void saveYRecuperar_persistenEntreLlamadas() {
var empleado = repository.save(new Empleado(null, "Eva",
"eva@certidevs.com", new BigDecimal("45000"), null));
var encontrado = repository.findById(empleado.getId());
assertThat(encontrado).isPresent();
}
}
Propagation.NOT_SUPPORTED desactiva la transacción del test; cada llamada al repositorio crea y cierra la suya. Útil para verificar comportamiento real de auditoría JPA, optimistic locking o eventos JPA.
Una alternativa más quirúrgica es
@Rollback(false)en métodos puntuales si solo un test necesita ver el commit. Cuidado: deja datos en la BD si no se hace cleanup.
Buenas prácticas
- Por defecto deja H2 in-memory para tests rápidos de queries derivadas simples. Cuando uses funciones específicas de PostgreSQL (jsonb, full-text search, window functions), pasa a Testcontainers.
- Encapsula Testcontainers en una clase abstracta
AbstractPostgresTesty haz que los tests la extiendan. El contenedor se reutiliza durante toda la suite. - Usa
TestEntityManagerpara preparar fixtures, no el repositorio. Si elsaveestá roto, no quieres que tu fixture también lo esté. - Combina
em.flush()yem.clear()antes del act: garantiza que la query de prueba realmente toca la BD. - Nombra los tests con el patrón
metodo_condicion_resultado:findByEmail_cuandoNoExiste_devuelveVacio. La salida en el reporte se lee como una especificación. - Mantén los
.sqlde@Sqlversionados junto al test (mismo paquete) y con nombres descriptivos. - No abuses de
@Sql: si el dataset cambia mucho, el script se vuelve frágil. Para tests aislados, prefiereTestEntityManager. - Cuando uses Testcontainers, fija la versión exacta (
postgres:17-alpine) para que la BD de tests sea reproducible. - Asegúrate de que el pool de conexiones de tests es pequeño (
spring.datasource.hikari.maximum-pool-size: 2) para evitar sobrecargar la BD efímera.
@DataJpaTestte permite tener confianza en la capa de persistencia con tests rápidos y específicos. Combinado con Testcontainers para PostgreSQL real, los tests pasan a representar lo que de verdad ocurrirá en producción. La inversión de configurar Testcontainers se amortiza la primera vez que un test detecta un fallo de SQL nativo antes del despliegue.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Boot
Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Aplicar @DataJpaTest para probar repositorios sin levantar todo el contexto. Preparar fixtures con TestEntityManager y @Sql. Sustituir H2 por PostgreSQL real con Testcontainers y @AutoConfigureTestDatabase replace=NONE. Validar queries derivadas, @Query JPQL y Specifications. Gestionar el rollback automático y forzar transacciones explícitas cuando hagan falta.