Slice tests con @DataJpaTest

Avanzado
Spring Boot
Spring Boot
Actualizado: 07/05/2026

Diagrama: tutorial-spring-boot-test-slices-datajpatest

Qué carga @DataJpaTest

@DataJpaTest autoconfigura todo lo necesario para trabajar con JPA y nada más:

  • Entities con @Entity.
  • Repositorios @Repository y los que extienden JpaRepository, CrudRepository, etc.
  • EntityManager y TestEntityManager.
  • 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 el PersistenceContext para que la siguiente lectura realmente vaya a BD y no devuelva el objeto cacheado.
  • El assertThat viene 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 el DataSource que se le proporcione.
  • @ServiceConnection (Spring Boot 4) configura automáticamente la URL, usuario y contraseña del contenedor en el contexto. Sustituye el viejo @DynamicPropertySource con boilerplate.
  • @Testcontainers y @Container son 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ón static), 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 AbstractPostgresTest y haz que los tests la extiendan. El contenedor se reutiliza durante toda la suite.
  • Usa TestEntityManager para preparar fixtures, no el repositorio. Si el save está roto, no quieres que tu fixture también lo esté.
  • Combina em.flush() y em.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 .sql de @Sql versionados 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, prefiere TestEntityManager.
  • 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.

@DataJpaTest te 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 - Autor del tutorial

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.