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();
}
}