feat: add spring-boot-testing skill for Spring Boot 4.0 (#1085)

- Introduced MockMvcTester for AssertJ-style assertions in Spring MVC testing.
- Added @RestClientTest for testing REST clients with MockRestServiceServer.
- Implemented RestTestClient as a modern alternative to TestRestTemplate.
- Documented migration steps from Spring Boot 3.x to 4.0, including dependency and annotation changes.
- Created an overview of test slices to guide testing strategies.
- Included Testcontainers setup for JDBC testing with PostgreSQL and MySQL.
- Enhanced @WebMvcTest documentation with examples for various HTTP methods and validation.
This commit is contained in:
Kartik Dhiman
2026-03-20 04:54:37 +05:30
committed by GitHub
parent ee71c0689c
commit e4fc57f204
16 changed files with 3171 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
# @DataJpaTest
Testing JPA repositories with isolated data layer slice.
## Basic Structure
```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18");
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
}
```
## What Gets Loaded
- Repository beans
- EntityManager / TestEntityManager
- DataSource
- Transaction manager
- No web layer, no services, no controllers
## Testing Custom Queries
```java
@Test
void shouldFindOrdersByStatus() {
// Given - Using var for cleaner code
var pending = new Order("PENDING");
var completed = new Order("COMPLETED");
entityManager.persist(pending);
entityManager.persist(completed);
entityManager.flush();
// When
var pendingOrders = orderRepository.findByStatus("PENDING");
// Then - Using sequenced collection methods
assertThat(pendingOrders).hasSize(1);
assertThat(pendingOrders.getFirst().getStatus()).isEqualTo("PENDING");
}
```
## Testing Native Queries
```java
@Test
void shouldExecuteNativeQuery() {
entityManager.persist(new Order("PENDING", BigDecimal.valueOf(100)));
entityManager.persist(new Order("PENDING", BigDecimal.valueOf(200)));
entityManager.flush();
var total = orderRepository.calculatePendingTotal();
assertThat(total).isEqualTo(new BigDecimal("300.00"));
}
```
## Testing Pagination
```java
@Test
void shouldReturnPagedResults() {
// Insert 20 orders using IntStream
IntStream.range(0, 20).forEach(i -> {
entityManager.persist(new Order("PENDING"));
});
entityManager.flush();
var page = orderRepository.findByStatus("PENDING", PageRequest.of(0, 10));
assertThat(page.getContent()).hasSize(10);
assertThat(page.getTotalElements()).isEqualTo(20);
assertThat(page.getContent().getFirst().getStatus()).isEqualTo("PENDING");
}
```
## Testing Lazy Loading
```java
@Test
void shouldLazyLoadOrderItems() {
var order = new Order("PENDING");
order.addItem(new OrderItem("Product", 2));
entityManager.persist(order);
entityManager.flush();
entityManager.clear(); // Detach from persistence context
var found = orderRepository.findById(order.getId());
assertThat(found).isPresent();
// This will trigger lazy loading
assertThat(found.get().getItems()).hasSize(1);
assertThat(found.get().getItems().getFirst().getProduct()).isEqualTo("Product");
}
```
## Testing Cascading
```java
@Test
void shouldCascadeDelete() {
var order = new Order("PENDING");
order.addItem(new OrderItem("Product", 2));
entityManager.persist(order);
entityManager.flush();
orderRepository.delete(order);
entityManager.flush();
assertThat(entityManager.find(OrderItem.class, order.getItems().getFirst().getId()))
.isNull();
}
```
## Testing @Query Methods
```java
@Query("SELECT o FROM Order o WHERE o.createdAt > :date AND o.status = :status")
List<Order> findRecentByStatus(@Param("date") LocalDateTime date,
@Param("status") String status);
@Test
void shouldFindRecentOrders() {
var old = new Order("PENDING");
old.setCreatedAt(LocalDateTime.now().minusDays(10));
var recent = new Order("PENDING");
recent.setCreatedAt(LocalDateTime.now().minusHours(1));
entityManager.persist(old);
entityManager.persist(recent);
entityManager.flush();
var recentOrders = orderRepository.findRecentByStatus(
LocalDateTime.now().minusDays(1), "PENDING");
assertThat(recentOrders).hasSize(1);
assertThat(recentOrders.getFirst().getId()).isEqualTo(recent.getId());
}
```
## Using H2 vs Real Database
### H2 (Default - Not Recommended for Production Parity)
```java
@DataJpaTest // Uses embedded H2 by default
class OrderRepositoryH2Test {
// Fast but may miss DB-specific issues
}
```
### Testcontainers (Recommended)
```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryPostgresTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18");
}
```
## Transaction Behavior
Tests are @Transactional by default and roll back after each test.
```java
@Test
@Rollback(false) // Don't roll back (rarely needed)
void shouldPersistData() {
orderRepository.save(new Order("PENDING"));
// Data will remain in database after test
}
```
## Key Points
1. Use TestEntityManager for setup data
2. Always flush() after persist() to trigger SQL
3. Clear() the entity manager to test lazy loading
4. Use real database (Testcontainers) for accurate results
5. Test both success and failure cases
6. Leverage Java 25 var keyword for cleaner variable declarations
7. Use sequenced collection methods (getFirst(), getLast(), reversed())