Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:30 +08:00
commit 171acedaa4
220 changed files with 85967 additions and 0 deletions

View File

@@ -0,0 +1,626 @@
---
name: spring-boot-test-patterns
description: Comprehensive testing patterns for Spring Boot applications including unit, integration, slice, and container-based testing with JUnit 5, Mockito, Testcontainers, and performance optimization. Use when implementing robust test suites for Spring Boot applications.
category: testing
tags: [spring-boot, java, testing, junit5, mockito, testcontainers, integration-testing, unit-testing, test-slices]
version: 1.5.0
language: java
license: Complete terms in LICENSE.txt
allowed-tools: Read, Write, Bash
---
# Spring Boot Testing Patterns
## Overview
This skill provides comprehensive guidance for writing robust test suites for Spring Boot applications. It covers unit testing with Mockito, integration testing with Testcontainers, performance-optimized slice testing patterns, and best practices for maintaining fast feedback loops.
## When to Use This Skill
Use this skill when:
- Writing unit tests for services, repositories, or utilities
- Implementing integration tests with real databases using Testcontainers
- Setting up performance-optimized test slices (@DataJpaTest, @WebMvcTest)
- Configuring Spring Boot 3.5+ @ServiceConnection for container management
- Testing REST APIs with MockMvc, TestRestTemplate, or WebTestClient
- Optimizing test performance through context caching and container reuse
- Setting up CI/CD pipelines for integration tests
- Implementing comprehensive test strategies for monolithic or microservices applications
## Core Concepts
### Test Architecture Philosophy
Spring Boot testing follows a layered approach with distinct test types:
**1. Unit Tests**
- Fast, isolated tests without Spring context
- Use Mockito for dependency injection
- Focus on business logic validation
- Target completion time: < 50ms per test
**2. Slice Tests**
- Minimal Spring context loading for specific layers
- Use @DataJpaTest for repository tests
- Use @WebMvcTest for controller tests
- Use @WebFluxTest for reactive controller tests
- Target completion time: < 100ms per test
**3. Integration Tests**
- Full Spring context with real dependencies
- Use @SpringBootTest with @ServiceConnection containers
- Test complete application flows
- Target completion time: < 500ms per test
### Key Testing Annotations
**Spring Boot Test Annotations:**
- `@SpringBootTest`: Load full application context (use sparingly)
- `@DataJpaTest`: Load only JPA components (repositories, entities)
- `@WebMvcTest`: Load only MVC layer (controllers, @ControllerAdvice)
- `@WebFluxTest`: Load only WebFlux layer (reactive controllers)
- `@JsonTest`: Load only JSON serialization components
**Testcontainer Annotations:**
- `@ServiceConnection`: Wire Testcontainer to Spring Boot test (Spring Boot 3.5+)
- `@DynamicPropertySource`: Register dynamic properties at runtime
- `@Testcontainers`: Enable Testcontainers lifecycle management
## Dependencies
### Maven Dependencies
```xml
<dependencies>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<!-- Additional Testing Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
```
### Gradle Dependencies
```kotlin
dependencies {
// Spring Boot Test Starter
testImplementation("org.springframework.boot:spring-boot-starter-test")
// Testcontainers
testImplementation("org.testcontainers:junit-jupiter:1.19.0")
testImplementation("org.testcontainers:postgresql:1.19.0")
// Additional Dependencies
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
}
```
## Instructions
### Unit Testing Pattern
Test business logic with mocked dependencies:
```java
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void shouldFindUserByIdWhenExists() {
// Arrange
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setEmail("test@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
Optional<User> result = userService.findById(userId);
// Assert
assertThat(result).isPresent();
assertThat(result.get().getEmail()).isEqualTo("test@example.com");
verify(userRepository, times(1)).findById(userId);
}
}
```
### Slice Testing Pattern
Use focused test slices for specific layers:
```java
// Repository test with minimal context
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestContainerConfig
public class UserRepositoryIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndRetrieveUserFromDatabase() {
// Arrange
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
// Act
User saved = userRepository.save(user);
userRepository.flush();
Optional<User> retrieved = userRepository.findByEmail("test@example.com");
// Assert
assertThat(retrieved).isPresent();
assertThat(retrieved.get().getName()).isEqualTo("Test User");
}
}
```
### REST API Testing Pattern
Test controllers with MockMvc for faster execution:
```java
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserService userService;
@Test
void shouldCreateUserAndReturn201() throws Exception {
User user = new User();
user.setEmail("newuser@example.com");
user.setName("New User");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.email").value("newuser@example.com"))
.andExpect(jsonPath("$.name").value("New User"));
}
}
```
### Testcontainers with @ServiceConnection
Configure containers with Spring Boot 3.5+:
```java
@TestConfiguration
public class TestContainerConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"))
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
}
}
```
## Examples
### Basic Unit Test
```java
@Test
void shouldCalculateTotalPrice() {
// Arrange
OrderItem item1 = new OrderItem();
item1.setPrice(10.0);
item1.setQuantity(2);
OrderItem item2 = new OrderItem();
item2.setPrice(15.0);
item2.setQuantity(1);
List<OrderItem> items = List.of(item1, item2);
// Act
double total = orderService.calculateTotal(items);
// Assert
assertThat(total).isEqualTo(35.0);
}
```
### Integration Test with Testcontainers
```java
@SpringBootTest
@TestContainerConfig
public class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Autowired
private UserRepository userRepository;
@MockBean
private PaymentService paymentService;
@Test
void shouldCreateOrderWithRealDatabase() {
// Arrange
User user = new User();
user.setEmail("customer@example.com");
user.setName("John Doe");
User savedUser = userRepository.save(user);
OrderRequest request = new OrderRequest();
request.setUserId(savedUser.getId());
request.setItems(List.of(
new OrderItemRequest(1L, 2),
new OrderItemRequest(2L, 1)
));
when(paymentService.processPayment(any())).thenReturn(true);
// Act
OrderResponse response = orderService.createOrder(request);
// Assert
assertThat(response.getOrderId()).isNotNull();
assertThat(response.getStatus()).isEqualTo("COMPLETED");
verify(paymentService, times(1)).processPayment(any());
}
}
```
### Reactive Test Pattern
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
public class ReactiveUserControllerIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Test
void shouldReturnUserAsJsonReactive() {
// Arrange
User user = new User();
user.setEmail("reactive@example.com");
user.setName("Reactive User");
// Act & Assert
webTestClient.get()
.uri("/api/users/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.email").isEqualTo("reactive@example.com")
.jsonPath("$.name").isEqualTo("Reactive User");
}
}
```
## Best Practices
### 1. Choose the Right Test Type
Select appropriate test annotations based on scope:
```java
// Use @DataJpaTest for repository-only tests (fastest)
@DataJpaTest
public class UserRepositoryTest { }
// Use @WebMvcTest for controller-only tests
@WebMvcTest(UserController.class)
public class UserControllerTest { }
// Use @SpringBootTest only for full integration testing
@SpringBootTest
public class UserServiceFullIntegrationTest { }
```
### 2. Use @ServiceConnection for Container Management
Prefer `@ServiceConnection` over manual `@DynamicPropertySource` for cleaner code:
```java
// Good - Spring Boot 3.5+
@TestConfiguration
public class TestConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
}
}
```
### 3. Keep Tests Deterministic
Always initialize test data explicitly:
```java
// Good - Explicit setup
@BeforeEach
void setUp() {
userRepository.deleteAll();
User user = new User();
user.setEmail("test@example.com");
userRepository.save(user);
}
// Avoid - Depending on other tests
@Test
void testUserExists() {
// Assumes previous test created a user
Optional<User> user = userRepository.findByEmail("test@example.com");
assertThat(user).isPresent();
}
```
### 4. Use Meaningful Assertions
Leverage AssertJ for readable, fluent assertions:
```java
// Good - Clear, readable assertions
assertThat(user.getEmail())
.isEqualTo("test@example.com");
assertThat(users)
.hasSize(3)
.contains(expectedUser);
// Avoid - JUnit assertions
assertEquals("test@example.com", user.getEmail());
assertTrue(users.size() == 3);
```
### 5. Organize Tests by Layer
Group related tests in separate classes to optimize context caching:
```java
// Repository tests (uses @DataJpaTest)
public class UserRepositoryTest { }
// Controller tests (uses @WebMvcTest)
public class UserControllerTest { }
// Service tests (uses mocks, no context)
public class UserServiceTest { }
// Full integration tests (uses @SpringBootTest)
public class UserFullIntegrationTest { }
```
## Performance Optimization
### Context Caching Strategy
Maximize Spring context caching by grouping tests with similar configurations:
```java
// Group repository tests with same configuration
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestContainerConfig
@TestPropertySource(properties = "spring.datasource.url=jdbc:postgresql:testdb")
public class UserRepositoryTest { }
// Group controller tests with same configuration
@WebMvcTest(UserController.class)
@AutoConfigureMockMvc
public class UserControllerTest { }
```
### Container Reuse Strategy
Reuse Testcontainers at JVM level for better performance:
```java
@Testcontainers
public class ContainerConfig {
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine"))
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@BeforeAll
static void startAll() {
POSTGRES.start();
}
@AfterAll
static void stopAll() {
POSTGRES.stop();
}
}
```
## Test Execution
### Maven Test Execution
```bash
# Run all tests
./mvnw test
# Run specific test class
./mvnw test -Dtest=UserServiceTest
# Run integration tests only
./mvnw test -Dintegration-test=true
# Run tests with coverage
./mvnw clean jacoco:prepare-agent test jacoco:report
```
### Gradle Test Execution
```bash
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests UserServiceTest
# Run integration tests only
./gradlew integrationTest
# Run tests with coverage
./gradlew test jacocoTestReport
```
## CI/CD Configuration
### GitHub Actions Example
```yaml
name: Spring Boot Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_USER: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-
- name: Run tests
run: ./mvnw test -Dspring.profiles.active=test
```
### Docker Compose for Local Testing
```yaml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```
## References
For detailed information, refer to the following resources:
- [API Reference](./references/api-reference.md) - Complete test annotations and utilities
- [Best Practices](./references/best-practices.md) - Testing patterns and optimization
- [Workflow Patterns](./references/workflow-patterns.md) - Complete integration test examples
## Related Skills
- **spring-boot-dependency-injection** - Unit testing patterns with constructor injection
- **spring-boot-rest-api-standards** - REST API patterns to test
- **spring-boot-crud-patterns** - CRUD patterns to test
- **unit-test-service-layer** - Advanced service layer testing techniques
## Performance Targets
- **Unit tests**: < 50ms per test
- **Slice tests**: < 100ms per test
- **Integration tests**: < 500ms per test
- **Maximize context caching** by grouping tests with same configuration
- **Reuse Testcontainers** at JVM level where possible
## Key Principles
1. Use test slices for focused, fast tests
2. Prefer @ServiceConnection on Spring Boot 3.5+
3. Keep tests deterministic with explicit setup
4. Mock external dependencies, use real databases
5. Avoid @DirtiesContext unless absolutely necessary
6. Organize tests by layer to optimize context reuse
This skill enables building comprehensive test suites that validate Spring Boot applications reliably while maintaining fast feedback loops for development.

View File

@@ -0,0 +1,74 @@
# Spring Boot Test API Reference
## Test Annotations
**Spring Boot Test Annotations:**
- `@SpringBootTest`: Load full application context (use sparingly)
- `@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)`: Full test with random HTTP port
- `@SpringBootTest(webEnvironment = WebEnvironment.MOCK)`: Full test with mock web environment
- `@DataJpaTest`: Load only JPA components (repositories, entities)
- `@WebMvcTest`: Load only MVC layer (controllers, @ControllerAdvice)
- `@WebFluxTest`: Load only WebFlux layer (reactive controllers)
- `@JsonTest`: Load only JSON serialization components
- `@RestClientTest`: Load only REST client components
- `@AutoConfigureMockMvc`: Provide MockMvc bean in @SpringBootTest
- `@AutoConfigureWebTestClient`: Provide WebTestClient bean for WebFlux tests
- `@AutoConfigureTestDatabase`: Control test database configuration
**Testcontainer Annotations:**
- `@ServiceConnection`: Wire Testcontainer to Spring Boot test (Spring Boot 3.5+)
- `@DynamicPropertySource`: Register dynamic properties at runtime
- `@Container`: Mark field as Testcontainer (requires @Testcontainers)
- `@Testcontainers`: Enable Testcontainers lifecycle management
**Test Lifecycle Annotations:**
- `@BeforeEach`: Run before each test method
- `@AfterEach`: Run after each test method
- `@BeforeAll`: Run once before all tests in class (must be static)
- `@AfterAll`: Run once after all tests in class (must be static)
- `@DisplayName`: Custom test name for reports
- `@Disabled`: Skip test
- `@Tag`: Tag tests for selective execution
**Test Isolation Annotations:**
- `@DirtiesContext`: Clear Spring context after test (forces rebuild)
- `@DirtiesContext(classMode = ClassMode.AFTER_CLASS)`: Clear after entire class
## Common Test Utilities
**MockMvc Methods:**
- `mockMvc.perform(get("/path"))`: Perform GET request
- `mockMvc.perform(post("/path")).contentType(MediaType.APPLICATION_JSON)`: POST with content type
- `.andExpect(status().isOk())`: Assert HTTP status
- `.andExpect(content().contentType("application/json"))`: Assert content type
- `.andExpect(jsonPath("$.field").value("expected"))`: Assert JSON path value
**TestRestTemplate Methods:**
- `restTemplate.getForEntity("/path", String.class)`: GET request
- `restTemplate.postForEntity("/path", body, String.class)`: POST request
- `response.getStatusCode()`: Get HTTP status
- `response.getBody()`: Get response body
**WebTestClient Methods (Reactive):**
- `webTestClient.get().uri("/path").exchange()`: Perform GET request
- `.expectStatus().isOk()`: Assert status
- `.expectBody().jsonPath("$.field").isEqualTo(value)`: Assert JSON
## Test Slices Performance Guidelines
- **Unit tests**: Complete in <50ms each
- **Integration tests**: Complete in <500ms each
- **Maximize context caching** by grouping tests with same configuration
- **Reuse Testcontainers** at JVM level where possible
## Common Test Annotations Reference
| Annotation | Purpose | When to Use |
|------------|---------|-------------|
| `@SpringBootTest` | Full application context | Full integration tests only |
| `@DataJpaTest` | JPA components only | Repository and entity tests |
| `@WebMvcTest` | MVC layer only | Controller tests |
| `@WebFluxTest` | WebFlux layer only | Reactive controller tests |
| `@ServiceConnection` | Container integration | Spring Boot 3.5+ with Testcontainers |
| `@DynamicPropertySource` | Dynamic properties | Pre-3.5 or custom configuration |
| `@DirtiesContext` | Context cleanup | When absolutely necessary |

View File

@@ -0,0 +1,263 @@
# Spring Boot Testing Best Practices
## Choose the Right Test Type
Select the most efficient test annotation for your use case:
```java
// Use @DataJpaTest for repository-only tests (fastest)
@DataJpaTest
public class UserRepositoryTest { }
// Use @WebMvcTest for controller-only tests
@WebMvcTest(UserController.class)
public class UserControllerTest { }
// Use @SpringBootTest only for full integration testing
@SpringBootTest
public class UserServiceFullIntegrationTest { }
```
## Use @ServiceConnection for Container Management (Spring Boot 3.5+)
Prefer `@ServiceConnection` over manual `@DynamicPropertySource` for cleaner code:
```java
// Good - Spring Boot 3.5+
@TestConfiguration
public class TestConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
}
}
// Avoid - Manual property registration
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
// ... more properties
}
```
## Keep Tests Deterministic
Always initialize test data explicitly and never depend on test execution order:
```java
// Good - Explicit setup
@BeforeEach
void setUp() {
userRepository.deleteAll();
User user = new User();
user.setEmail("test@example.com");
userRepository.save(user);
}
// Avoid - Depending on other tests
@Test
void testUserExists() {
// Assumes previous test created a user
Optional<User> user = userRepository.findByEmail("test@example.com");
assertThat(user).isPresent();
}
```
## Use Transactional Tests Carefully
Mark test classes with `@Transactional` for automatic rollback, but understand the implications:
```java
@SpringBootTest
@Transactional // Automatically rolls back after each test
public class UserControllerIntegrationTest {
@Test
void shouldCreateUser() throws Exception {
// Changes will be rolled back after test
mockMvc.perform(post("/api/users")....)
.andExpect(status().isCreated());
}
}
```
**Note**: Be aware that `@Transactional` test behavior may differ from production due to lazy loading and flush semantics.
## Organize Tests by Layer
Group related tests in separate classes to optimize context caching:
```java
// Repository tests (uses @DataJpaTest)
public class UserRepositoryTest { }
// Controller tests (uses @WebMvcTest)
public class UserControllerTest { }
// Service tests (uses mocks, no context)
public class UserServiceTest { }
// Full integration tests (uses @SpringBootTest)
public class UserFullIntegrationTest { }
```
## Use Meaningful Assertions
Leverage AssertJ for readable, fluent assertions:
```java
// Good - Clear, readable assertions
assertThat(user.getEmail())
.isEqualTo("test@example.com");
assertThat(users)
.hasSize(3)
.contains(expectedUser);
assertThatThrownBy(() -> userService.save(invalidUser))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("Email is required");
// Avoid - JUnit assertions
assertEquals("test@example.com", user.getEmail());
assertTrue(users.size() == 3);
```
## Mock External Dependencies
Mock external services but use real databases for integration tests:
```java
// Good - Mock external services, use real DB
@SpringBootTest
@TestContainerConfig.class
public class OrderServiceTest {
@MockBean
private EmailService emailService;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldSendConfirmationEmail() {
// Use real database, mock email service
Order order = new Order();
orderService.createOrder(order);
verify(emailService, times(1)).sendConfirmation(order);
}
}
// Avoid - Mocking the database layer
@Test
void shouldCreateOrder() {
when(orderRepository.save(any())).thenReturn(mockOrder);
// Tests don't verify actual database behavior
}
```
## Use Test Fixtures for Common Data
Create reusable test data builders:
```java
public class UserTestFixture {
public static User validUser() {
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
return user;
}
public static User userWithEmail(String email) {
User user = validUser();
user.setEmail(email);
return user;
}
}
// Usage in tests
@Test
void shouldSaveUser() {
User user = UserTestFixture.validUser();
userRepository.save(user);
assertThat(userRepository.count()).isEqualTo(1);
}
```
## Document Complex Test Scenarios
Use `@DisplayName` and comments for complex test logic:
```java
@Test
@DisplayName("Should validate email format and reject duplicates with proper error message")
void shouldValidateEmailBeforePersisting() {
// Given: Two users with the same email
User user1 = new User();
user1.setEmail("test@example.com");
userRepository.save(user1);
User user2 = new User();
user2.setEmail("test@example.com"); // Duplicate email
// When: Attempting to save duplicate
// Then: Should throw exception with clear message
assertThatThrownBy(() -> {
userRepository.save(user2);
userRepository.flush();
})
.isInstanceOf(DataIntegrityViolationException.class)
.hasMessageContaining("unique constraint");
}
```
## Avoid Common Pitfalls
```java
// Avoid: Using @DirtiesContext without reason (forces context rebuild)
@SpringBootTest
@DirtiesContext // DON'T USE unless absolutely necessary
public class ProblematicTest { }
// Avoid: Mixing multiple profiles in same test suite
@SpringBootTest(properties = "spring.profiles.active=dev,test,prod")
public class MultiProfileTest { }
// Avoid: Starting containers manually
@SpringBootTest
public class ManualContainerTest {
static {
PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
postgres.start(); // Avoid - use @ServiceConnection instead
}
}
// Good: Consistent configuration, minimal context switching
@SpringBootTest
@TestContainerConfig
public class ProperTest { }
```
## Test Naming Conventions
Convention: Use descriptive method names that start with `should` or `test` to make test intent explicit.
**Naming Rules:**
- **Prefix**: Start with `should` or `test` to clearly indicate test purpose
- **Structure**: Use camelCase for readability (no underscores)
- **Clarity**: Name should indicate what is being tested and the expected outcome
- **Example pattern**: `should[ExpectedBehavior]When[Condition]()`
**Examples:**
```
shouldReturnUsersJson()
shouldThrowNotFoundWhenIdDoesntExist()
shouldPropagateExceptionOnPersistenceError()
shouldSaveAndRetrieveUserFromDatabase()
shouldValidateEmailFormatBeforePersisting()
```
Apply these rules consistently across all integration test methods.

View File

@@ -0,0 +1,340 @@
# Spring Boot Testing Workflow Patterns
## Complete Database Integration Test Pattern
**Scenario**: Test a JPA repository with a real PostgreSQL database using Testcontainers.
```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestContainerConfig
public class UserRepositoryIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndRetrieveUserFromDatabase() {
// Arrange
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
// Act
User saved = userRepository.save(user);
userRepository.flush();
Optional<User> retrieved = userRepository.findByEmail("test@example.com");
// Assert
assertThat(retrieved).isPresent();
assertThat(retrieved.get().getName()).isEqualTo("Test User");
}
@Test
void shouldThrowExceptionForDuplicateEmail() {
// Arrange
User user1 = new User();
user1.setEmail("duplicate@example.com");
user1.setName("User 1");
User user2 = new User();
user2.setEmail("duplicate@example.com");
user2.setName("User 2");
userRepository.save(user1);
// Act & Assert
assertThatThrownBy(() -> {
userRepository.save(user2);
userRepository.flush();
}).isInstanceOf(DataIntegrityViolationException.class);
}
}
```
## Complete REST API Integration Test Pattern
**Scenario**: Test REST controllers with full Spring context using MockMvc.
```java
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldCreateUserAndReturn201() throws Exception {
User user = new User();
user.setEmail("newuser@example.com");
user.setName("New User");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.email").value("newuser@example.com"))
.andExpect(jsonPath("$.name").value("New User"));
}
@Test
void shouldReturnUserById() throws Exception {
// Arrange
User user = new User();
user.setEmail("existing@example.com");
user.setName("Existing User");
User saved = userRepository.save(user);
// Act & Assert
mockMvc.perform(get("/api/users/" + saved.getId())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("existing@example.com"))
.andExpect(jsonPath("$.name").value("Existing User"));
}
@Test
void shouldReturnNotFoundForMissingUser() throws Exception {
mockMvc.perform(get("/api/users/99999")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
@Test
void shouldUpdateUserAndReturn200() throws Exception {
// Arrange
User user = new User();
user.setEmail("update@example.com");
user.setName("Original Name");
User saved = userRepository.save(user);
User updateData = new User();
updateData.setName("Updated Name");
// Act & Assert
mockMvc.perform(put("/api/users/" + saved.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateData)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Updated Name"));
}
@Test
void shouldDeleteUserAndReturn204() throws Exception {
// Arrange
User user = new User();
user.setEmail("delete@example.com");
user.setName("To Delete");
User saved = userRepository.save(user);
// Act & Assert
mockMvc.perform(delete("/api/users/" + saved.getId()))
.andExpect(status().isNoContent());
assertThat(userRepository.findById(saved.getId())).isEmpty();
}
}
```
## Service Layer Integration Test Pattern
**Scenario**: Test business logic with mocked repository.
```java
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void shouldFindUserByIdWhenExists() {
// Arrange
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setEmail("test@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
Optional<User> result = userService.findById(userId);
// Assert
assertThat(result).isPresent();
assertThat(result.get().getEmail()).isEqualTo("test@example.com");
verify(userRepository, times(1)).findById(userId);
}
@Test
void shouldReturnEmptyWhenUserNotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act
Optional<User> result = userService.findById(userId);
// Assert
assertThat(result).isEmpty();
verify(userRepository, times(1)).findById(userId);
}
@Test
void shouldThrowExceptionWhenSavingInvalidUser() {
// Arrange
User invalidUser = new User();
invalidUser.setEmail("invalid-email");
when(userRepository.save(invalidUser))
.thenThrow(new DataIntegrityViolationException("Invalid email"));
// Act & Assert
assertThatThrownBy(() -> userService.save(invalidUser))
.isInstanceOf(DataIntegrityViolationException.class);
}
}
```
## Reactive WebFlux Integration Test Pattern
**Scenario**: Test WebFlux controllers with WebTestClient.
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
public class ReactiveUserControllerIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldReturnUserAsJsonReactive() {
// Arrange
User user = new User();
user.setEmail("reactive@example.com");
user.setName("Reactive User");
User saved = userRepository.save(user);
// Act & Assert
webTestClient.get()
.uri("/api/users/" + saved.getId())
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.email").isEqualTo("reactive@example.com")
.jsonPath("$.name").isEqualTo("Reactive User");
}
@Test
void shouldReturnArrayOfUsers() {
// Arrange
User user1 = new User();
user1.setEmail("user1@example.com");
user1.setName("User 1");
User user2 = new User();
user2.setEmail("user2@example.com");
user2.setName("User 2");
userRepository.saveAll(List.of(user1, user2));
// Act & Assert
webTestClient.get()
.uri("/api/users")
.exchange()
.expectStatus().isOk()
.expectBodyList(User.class)
.hasSize(2);
}
}
```
## Testcontainers Configuration Patterns
### @ServiceConnection Pattern (Spring Boot 3.5+)
```java
@TestConfiguration
public class TestContainerConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"))
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
// Do not call start(); Spring Boot will manage lifecycle for @ServiceConnection beans
}
}
```
### @DynamicPropertySource Pattern (Legacy)
```java
public class SharedContainers {
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"))
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@BeforeAll
static void startAll() {
POSTGRES.start();
}
@AfterAll
static void stopAll() {
POSTGRES.stop();
}
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
}
```
### Slice Tests with Testcontainers
```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestContainerConfig
public class MyRepositoryIntegrationTest {
// repository tests
}
```