Spring BootDataJpaTest
Spring Boot

DataJpaTest

DataJpaTest is a Spring Boot test slice that loads only the JPA layer — entities, repositories, and the EntityManager — without the web layer or service beans. By default it uses an embedded H2 database and rolls back each test in a transaction. Combined with Testcontainers and @AutoConfigureTestDatabase it can run against a real database, catching SQL dialect differences that H2 hides.

Basic DataJpaTest Setup

@DataJpaTest auto-configures an embedded H2 database, scans @Entity classes and @Repository interfaces, and wraps each test in a transaction that rolls back automatically. This makes tests isolated and fast — no leftover data between tests, no container startup overhead.
Java
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;  // JPA helper for test setup

    // ── Save and find: ────────────────────────────────────────────────
    @Test
    void save_persistsUser() {
        User user = User.builder()
            .name("Alice")
            .email("alice@example.com")
            .role(Role.USER)
            .build();

        User saved = userRepository.save(user);

        assertThat(saved.getId()).isNotNull();
        assertThat(saved.getCreatedAt()).isNotNull();
    }

    // ── TestEntityManager for setup — bypasses repo layer: ────────────
    @Test
    void findByEmail_returnsUser() {
        // Persist directly via EntityManager (bypasses repo validation):
        entityManager.persistAndFlush(User.builder()
            .name("Bob")
            .email("bob@example.com")
            .role(Role.USER)
            .build());

        Optional<User> found = userRepository.findByEmail("bob@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Bob");
    }

    @Test
    void findByEmail_notFound_returnsEmpty() {
        Optional<User> found =
            userRepository.findByEmail("nobody@example.com");
        assertThat(found).isEmpty();
    }

    // ── Custom JPQL query: ────────────────────────────────────────────
    @Test
    void findByRole_returnsMatchingUsers() {
        entityManager.persist(User.builder()
            .name("Admin1").email("a1@x.com").role(Role.ADMIN).build());
        entityManager.persist(User.builder()
            .name("Admin2").email("a2@x.com").role(Role.ADMIN).build());
        entityManager.persist(User.builder()
            .name("User1").email("u1@x.com").role(Role.USER).build());
        entityManager.flush();

        List<User> admins = userRepository.findByRole(Role.ADMIN);

        assertThat(admins).hasSize(2)
            .extracting(User::getRole)
            .containsOnly(Role.ADMIN);
    }
}

Testing with Real PostgreSQL via Testcontainers

H2 silently accepts SQL that PostgreSQL rejects — JSON column types, full-text search, window functions, and database-specific functions all behave differently. Use @AutoConfigureTestDatabase(replace = NONE) with a Testcontainers PostgreSQL container to test against the actual target database.
Java
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryPostgresTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url",      postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private TestEntityManager entityManager;

    // ── PostgreSQL-specific: native query with JSONB: ─────────────────
    @Test
    void findOrdersWithMetadata_usesJsonbQuery() {
        Order order = Order.builder()
            .userId(1L)
            .status(OrderStatus.COMPLETED)
            .metadata("{"channel":"mobile"}")
            .build();
        entityManager.persistAndFlush(order);

        List<Order> mobileOrders =
            orderRepository.findByMetadataChannel("mobile");

        assertThat(mobileOrders).hasSize(1);
    }

    // ── Pagination: ───────────────────────────────────────────────────
    @Test
    void findByUserId_withPagination_returnsCorrectPage() {
        Long userId = 10L;
        for (int i = 0; i < 15; i++) {
            entityManager.persist(Order.builder()
                .userId(userId)
                .status(OrderStatus.COMPLETED)
                .build());
        }
        entityManager.flush();

        Page<Order> page = orderRepository.findByUserId(
            userId, PageRequest.of(0, 10, Sort.by("createdAt").descending()));

        assertThat(page.getTotalElements()).isEqualTo(15);
        assertThat(page.getContent()).hasSize(10);
        assertThat(page.getTotalPages()).isEqualTo(2);
    }

    // ── Aggregate query: ──────────────────────────────────────────────
    @Test
    void sumOrderValueByUser_returnsCorrectTotal() {
        entityManager.persist(Order.builder()
            .userId(1L).totalAmount(new BigDecimal("50.00")).build());
        entityManager.persist(Order.builder()
            .userId(1L).totalAmount(new BigDecimal("30.00")).build());
        entityManager.persist(Order.builder()
            .userId(2L).totalAmount(new BigDecimal("100.00")).build());
        entityManager.flush();

        BigDecimal total = orderRepository.sumTotalAmountByUserId(1L);

        assertThat(total).isEqualByComparingTo("80.00");
    }
}

Testing Entity Constraints and Relationships

DataJpaTest is the right place to verify that database-level constraints (unique, not-null, foreign key) and JPA relationship mappings (OneToMany, ManyToOne, cascade behaviour) work correctly. These constraints are enforced by the real database, not by Java validation.
Java
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class EntityConstraintTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url",      postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired private UserRepository userRepository;
    @Autowired private OrderRepository orderRepository;
    @Autowired private TestEntityManager entityManager;

    // ── Unique constraint: ────────────────────────────────────────────
    @Test
    void save_duplicateEmail_throwsException() {
        entityManager.persistAndFlush(
            User.builder().email("alice@x.com").name("Alice").build());

        assertThatThrownBy(() ->
            userRepository.saveAndFlush(
                User.builder().email("alice@x.com").name("Alice2").build()))
            .isInstanceOf(DataIntegrityViolationException.class);
    }

    // ── Not-null constraint: ──────────────────────────────────────────
    @Test
    void save_nullEmail_throwsException() {
        assertThatThrownBy(() ->
            userRepository.saveAndFlush(
                User.builder().name("NoEmail").build()))
            .isInstanceOf(DataIntegrityViolationException.class);
    }

    // ── OneToMany cascade — saving parent saves children: ─────────────
    @Test
    void saveOrder_withItems_cascadeSavesItems() {
        Order order = Order.builder()
            .userId(1L)
            .items(List.of(
                OrderItem.builder().productId(10L).quantity(2).build(),
                OrderItem.builder().productId(11L).quantity(1).build()
            ))
            .build();

        Order saved = orderRepository.save(order);
        entityManager.flush();
        entityManager.clear();  // clear cache to force reload from DB

        Order reloaded = orderRepository.findById(saved.getId())
            .orElseThrow();
        assertThat(reloaded.getItems()).hasSize(2);
    }

    // ── Orphan removal — removing item from collection deletes it: ────
    @Test
    void removeOrderItem_orphanRemovalDeletesIt() {
        Order order = orderRepository.save(Order.builder()
            .userId(1L)
            .items(new ArrayList<>(List.of(
                OrderItem.builder().productId(10L).quantity(1).build())))
            .build());
        entityManager.flush();

        order.getItems().clear();
        orderRepository.save(order);
        entityManager.flush();
        entityManager.clear();

        Order reloaded =
            orderRepository.findById(order.getId()).orElseThrow();
        assertThat(reloaded.getItems()).isEmpty();
    }
}

Testing Custom Queries and Specifications

DataJpaTest is the authoritative test for JPQL, native SQL, and Spring Data JPA Specification queries. These tests prove the query returns the right data for the right input — something a unit test with a mocked repository cannot verify.
Java
@DataJpaTest
class ProductRepositoryQueryTest {

    @Autowired private ProductRepository productRepository;
    @Autowired private TestEntityManager entityManager;

    @BeforeEach
    void setUp() {
        entityManager.persist(Product.builder()
            .name("Laptop").category("Electronics")
            .price(new BigDecimal("999.99")).inStock(true).build());
        entityManager.persist(Product.builder()
            .name("Phone").category("Electronics")
            .price(new BigDecimal("499.99")).inStock(true).build());
        entityManager.persist(Product.builder()
            .name("Desk").category("Furniture")
            .price(new BigDecimal("299.99")).inStock(false).build());
        entityManager.persist(Product.builder()
            .name("Chair").category("Furniture")
            .price(new BigDecimal("199.99")).inStock(true).build());
        entityManager.flush();
    }

    // ── Custom JPQL query: ────────────────────────────────────────────
    @Test
    void findByCategoryAndInStock_returnsMatchingProducts() {
        List<Product> result =
            productRepository.findByCategoryAndInStock(
                "Electronics", true);

        assertThat(result).hasSize(2)
            .extracting(Product::getName)
            .containsExactlyInAnyOrder("Laptop", "Phone");
    }

    // ── Price range query: ────────────────────────────────────────────
    @Test
    void findByPriceBetween_returnsCorrectRange() {
        List<Product> result = productRepository.findByPriceBetween(
            new BigDecimal("200.00"), new BigDecimal("600.00"));

        assertThat(result).hasSize(2)
            .extracting(Product::getName)
            .containsExactlyInAnyOrder("Phone", "Desk");
    }

    // ── JPA Specification (dynamic query): ───────────────────────────
    @Test
    void findAll_withSpecification_filtersCorrectly() {
        Specification<Product> spec =
            ProductSpecification.hasCategory("Furniture")
            .and(ProductSpecification.isInStock(true));

        List<Product> result = productRepository.findAll(spec);

        assertThat(result).hasSize(1)
            .first()
            .extracting(Product::getName)
            .isEqualTo("Chair");
    }

    // ── Exists query: ─────────────────────────────────────────────────
    @Test
    void existsByNameAndCategory_returnsTrue() {
        assertThat(productRepository
            .existsByNameAndCategory("Laptop", "Electronics")).isTrue();
        assertThat(productRepository
            .existsByNameAndCategory("Laptop", "Furniture")).isFalse();
    }
}