mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-20 16:15:12 +00:00
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:
197
skills/spring-boot-testing/references/datajpatest.md
Normal file
197
skills/spring-boot-testing/references/datajpatest.md
Normal 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())
|
||||
Reference in New Issue
Block a user