Spring Boot Integration Testing Best Practices
To create a new Cursor Rule:
- Enter the name as
sprint-boot-integration-testing
- Copy & paste the file content from below
For more information, visit the Project rules.
---
description:
globs:
alwaysApply: false
---
# Java Integration testing guidelines
These guidelines aim to ensure consistency, reliability, and maintainability of integration tests within the project.
## Implementing These Principles
These guidelines are built upon the following core principles:
- Principle 1: Test Isolation - Each integration test must be independent and not affect other tests
- Principle 2: Environment Reproducibility - Use containerized dependencies for consistent test environments
- Principle 3: Clear Test Boundaries - Focus on integration points rather than duplicating unit test logic
- Principle 4: Performance Optimization - Balance thorough testing with execution speed
- Principle 5: Maintainable Assertions - Use specific, clear assertions that provide meaningful feedback
- Principle 6: Resource Management - Properly manage external dependencies and cleanup after tests
## Table of contents
- Rule 1: Define Clear Scope and Purpose for Integration Tests
- Rule 2: Manage Test Environment & Dependencies with Testcontainers
- Rule 3: Utilize TestRestTemplate for Robust API Testing
- Rule 4: Implement Consistent Data Management Strategies
- Rule 5: Maintain Clear Test Structure and Assertions
- Rule 6: Optimize for Performance and Ensure Proper Cleanup
## Rule 1: Define Clear Scope and Purpose for Integration Tests
Title: Clearly Define the Scope and Purpose of Each Integration Test
Description:
- Integration tests must verify the interaction between multiple components or systems (e.g., service layer with database, service-to-service communication over HTTP).
- Clearly define the boundary of each integration test. What specific interaction, contract, or flow is being tested?
- Prefer integration tests for verifying contracts between services (APIs) and interactions with external dependencies (databases, message queues, etc.).
- Avoid replicating complex business logic in integration tests if it is already thoroughly covered by unit tests. Focus on the integration points.
**Good example:**
```java
// Assume: ProductService interacts with ProductRepository (database) and NotificationService (external HTTP)
// @SpringBootTest // or similar context for the test
// @Testcontainers // if using Testcontainers
public class ProductServiceIT {
private static final Logger log = LoggerFactory.getLogger(ProductServiceIT.class);
// @Autowired
// private ProductService productService;
// @Autowired
// private ProductRepository productRepository; // To verify DB state
// Mock or use a Testcontainer for NotificationService if its actual calls are out of scope
// @MockBean
// private NotificationService mockNotificationService;
// @Test
void should_createProduct_saveToDatabase_and_sendNotification() {
// Scope: Test the flow of creating a product, ensuring it's saved,
// and that a notification attempt is made.
// Given: A product DTO
// ProductDto newProductDto = new ProductDto("Laptop X1", 1500.00);
// When: ProductService creates the product
// Product createdProduct = productService.createProduct(newProductDto);
// Then: Verify interactions
// 1. Product is saved in the database (verify via repository or direct query)
// Optional<ProductEntity> savedEntity = productRepository.findById(createdProduct.getId());
// assertThat(savedEntity).isPresent();
// assertThat(savedEntity.get().getName()).isEqualTo("Laptop X1");
// 2. Notification service was called (verify via mock or wiremock if testing HTTP contract)
// verify(mockNotificationService).sendProductCreationNotification(any(Product.class));
log.info("Conceptual test: Product creation flow verified.");
}
}
Bad Example:
// @SpringBootTest
public class OverlappingProductLogicIT {
private static final Logger log = LoggerFactory.getLogger(OverlappingProductLogicIT.class);
// @Autowired
// private ProductService productService;
// @Test
void should_calculateComplexPricing_duringProductCreation() {
// Bad: This test might be re-testing complex pricing logic
// that should already be unit-tested in ProductService or a PricingEngine unit test.
// The integration test should focus on whether ProductService correctly integrates
// with the database and other services during creation, assuming pricing logic is correct.
// ProductDto productWithComplexPricing = new ProductDto("ComplexItem", 10.0, List.of(new DiscountRule(...)));
// Product createdProduct = productService.createProduct(productWithComplexPricing);
// If asserts here are deeply checking specific price calculations, it's likely a unit test concern.
// assertThat(createdProduct.getFinalPrice()).isEqualTo(9.99); // This might be too specific for an IT
log.warn("Conceptual bad test: Replicating unit test logic for pricing.");
}
}
Rule 2: Manage Test Environment & Dependencies with Testcontainers
Title: Use Testcontainers for Reliable Management of External Dependencies
Description:
- Use Testcontainers (org.testcontainers:testcontainers
) to manage external dependencies (databases, message brokers, caches, other services) required for the test. Avoid relying on pre-existing, shared external environments to ensure test isolation and reproducibility.
- Declare containerized dependencies using @Testcontainers
and @Container
annotations for JUnit 5 integration (org.testcontainers:junit-jupiter
). Manage container lifecycles appropriately (per test suite using static @Container
or per test method, favoring suite-level for performance).
- Use official or well-maintained Docker images for dependencies. Pin image versions (e.g., "postgres:15-alpine"
) to ensure reproducible builds.
- Configure containers programmatically (ports, environment variables, wait strategies) within the test setup. Use Wait.for...
strategies (e.g., Wait.forHttp("/health")
, Wait.forLogMessage(...)
) to ensure containers are ready before tests run.
- Inject dynamic container properties (like mapped ports or JDBC URLs) into the application context or test configuration. For Spring Boot, use @DynamicPropertySource
with a static method. For others, manually retrieve properties in setup methods.
Good example:
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat; // For assertion
@Testcontainers
@SpringBootTest // Or relevant test context setup
class MyRepositoryIT {
private static final Logger log = LoggerFactory.getLogger(MyRepositoryIT.class);
@Container // Static -> shared container for all tests in this class
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
// .waitingFor(Wait.forListeningPort()); // Default wait strategy is often sufficient for DBs
// Dynamically set properties based on container info
@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);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}
// Inject your repository/service here
// @Autowired
// private MyRepository repository;
@Test
void should_connectAndInteractWithDatabase() {
// Test logic interacting with the repository,
// which uses the Testcontainer database
assertThat(postgres.isRunning()).isTrue();
log.info("PostgreSQL container is running on JDBC URL: {}", postgres.getJdbcUrl());
// ... perform repository operations and assertions ...
// Example: MyEntity entity = new MyEntity("testData");
// repository.save(entity);
// Optional<MyEntity> found = repository.findById(entity.getId());
// assertThat(found).isPresent();
}
}
Bad Example:
// @SpringBootTest
public class MyServiceReliesOnExternalDbIT {
private static final Logger log = LoggerFactory.getLogger(MyServiceReliesOnExternalDbIT.class);
// @Autowired
// private MyDataService dataService;
// No Testcontainers. This test assumes an external PostgreSQL server
// is running on localhost:5432 with specific credentials and schema.
// spring.datasource.url=jdbc:postgresql://localhost:5432/mydb_dev
// spring.datasource.username=dev_user
// spring.datasource.password=dev_secret
// @Test
void should_fetchDataFromPreConfiguredExternalDatabase() {
// Bad: Test depends on an external, manually configured database.
// - Not isolated: Other tests or developers might change the DB state.
// - Not reproducible: Fails if DB is down, schema changes, or on CI without the DB.
// - Hard to manage data state between tests.
// List<Data> data = dataService.findAll();
// assertThat(data).isNotEmpty(); // This might pass or fail based on external DB state.
log.warn("Conceptual bad test: Relies on external, shared database.");
}
}
Rule 3: Utilize TestRestTemplate for Robust API Testing
Title: Employ TestRestTemplate for Testing RESTful APIs Following Arrange/Act/Assert
Description:
- Use Spring Boot's TestRestTemplate
(provided by spring-boot-starter-test
) for testing RESTful APIs in integration tests.
- Structure tests using the Arrange/Act/Assert pattern:
- Arrange: Set up request data, headers, authentication, and test prerequisites.
- Act: Perform the HTTP request using TestRestTemplate methods (getForEntity()
, postForEntity()
, exchange()
, etc.).
- Assert: Validate the response (status code, headers, response body) using AssertJ or JUnit assertions.
- Always validate the HTTP status code first using ResponseEntity.getStatusCode()
.
- Use AssertJ assertions for clear and fluent validation of response content. Be specific but avoid overly brittle assertions (e.g., don't assert entire large JSON bodies if only a few fields matter).
- Configure TestRestTemplate in a @BeforeEach
method or inject it via @Autowired
. For custom configurations (authentication, headers), use TestRestTemplate.withBasicAuth()
or create HttpEntity
with custom headers.
- Handle authentication consistently using TestRestTemplate's built-in methods or by setting appropriate headers in HttpEntity
.
- For complex request/response bodies, use POJOs that will be automatically serialized/deserialized by Spring's message converters (Jackson by default).
Good example:
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
// Assuming a simple DTO for request/response
class ResourceDto {
public int id;
public String name;
public String data;
public ResourceDto() {}
public ResourceDto(int id, String name, String data) { this.id = id; this.name = name; this.data = data;}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// Assume a controller exists at /resources that uses ResourceDto
// and has GET /resources/{id}, POST /resources
class MyApiControllerIT {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void getResourceById_shouldReturnOkAndResource() {
// Arrange: Set up the expected resource ID
int resourceId = 123;
// Act: Perform GET request
ResponseEntity<ResourceDto> response = restTemplate.getForEntity(
"/resources/{id}",
ResourceDto.class,
resourceId
);
// Assert: Validate response
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().id).isEqualTo(123);
assertThat(response.getBody().name).contains("ResourceName"); // Flexible assertion
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
void createResource_shouldReturnCreatedAndResourceLocation() {
// Arrange: Set up request data and headers
ResourceDto newResource = new ResourceDto(0, "New Item", "Some data");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<ResourceDto> request = new HttpEntity<>(newResource, headers);
// Act: Perform POST request
ResponseEntity<ResourceDto> response = restTemplate.postForEntity(
"/resources",
request,
ResourceDto.class
);
// Assert: Validate response
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().id).isNotNull(); // Assert that an ID was generated
assertThat(response.getBody().name).isEqualTo("New Item");
assertThat(response.getHeaders().getLocation()).isNotNull();
assertThat(response.getHeaders().getLocation().toString()).contains("/resources/");
}
}
Bad Example:
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApiTestAntiPatternsIT {
private static final Logger log = LoggerFactory.getLogger(ApiTestAntiPatternsIT.class);
// @Autowired private TestRestTemplate restTemplate;
@Test
void getResource_badAssertions() {
// Bad: Not checking status code first or at all.
// Bad: Extracting entire response as string and doing string manipulations.
// ResponseEntity<String> response = restTemplate.getForEntity("/resources/1", String.class);
// String responseBody = response.getBody();
// assertThat(responseBody).contains("\"id\":1"); // Brittle, hard to read, no status check
// Bad: Overly specific assertions on large JSON strings.
// assertThat(responseBody).isEqualTo("{very long and complex json string...}"); // Brittle
log.warn("Conceptual bad API test: Poor assertions, missing status code check.");
}
@Test
void createResource_noBodyValidation() {
// Bad: Not validating the structure or content of the response body upon creation.
// ResourceDto newResource = new ResourceDto(0, "Test", "data");
// ResponseEntity<Void> response = restTemplate.postForEntity("/resources", newResource, Void.class);
// assertThat(response.getStatusCode().value()).isEqualTo(201); // Only checks status code, ignores response body
log.warn("Conceptual bad API test: Missing response body validation.");
}
}
Rule 4: Implement Consistent Data Management Strategies
Title: Ensure Controlled and Isolated Data States for Each Test
Description:
- Each integration test must run with a known, controlled data state to ensure reliability and prevent interference between tests. Tests must be independent.
- Seed necessary test data before each test (@BeforeEach
) or test suite (@BeforeAll
). Options include:
- Application Services: Call repository or service methods to set up required entities.
- Object Mothers / Test Data Builders: Use patterns to create complex test data objects easily and consistently.
- SQL Scripts: Use @Sql
(Spring) or execute scripts via JDBC/Testcontainers execInContainer()
for setup.
- Clean up persistent data created during a test run to ensure test isolation. Choose one primary strategy:
- Transaction Rollback: (Preferred for simplicity if applicable, e.g., Spring Test with @Transactional
) Annotate test methods or the class. Spring Test will automatically roll back the transaction after each test for database operations within that transaction.
- Truncate/Delete Tables: Execute TRUNCATE TABLE ...
or DELETE FROM ...
statements in @AfterEach
or via Testcontainers. Fastest for complex state reset if transactions are not manageable across all interactions.
- Delete Specific Data: Use repository/service methods in @AfterEach
to delete only the data created by the test (can be complex to track and error-prone).
- Container Recreation: Recreate the database container per test or class (very slow, generally avoided unless absolutely necessary for complete isolation between test classes).
Good example:
(Using Spring Test @Transactional
for rollback)
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
// Assume Entity: Item with id, name
// Assume Repository: ItemRepository extends JpaRepository<Item, Long>
// @Testcontainers // If DB is managed by Testcontainers
// @SpringBootTest
// @Transactional // This will roll back transactions after each test method
public class ItemRepositoryTransactionalIT {
private static final Logger log = LoggerFactory.getLogger(ItemRepositoryTransactionalIT.class);
// @Autowired
// private ItemRepository itemRepository;
// @Test
void should_saveAndRetrieveItem() {
// Item newItem = new Item("Test Item");
// Item savedItem = itemRepository.save(newItem);
// assertThat(savedItem.getId()).isNotNull();
// Optional<Item> foundItem = itemRepository.findById(savedItem.getId());
// assertThat(foundItem).isPresent();
// assertThat(foundItem.get().getName()).isEqualTo("Test Item");
log.info("Conceptual test: Save and retrieve with @Transactional rollback.");
// Data inserted here will be rolled back automatically after this test method.
}
// @Test
void should_findNoItems_ifNoneSavedInThisTest() {
// List<Item> items = itemRepository.findAll();
// assertThat(items).isEmpty();
log.info("Conceptual test: Ensuring test isolation via @Transactional.");
// Due to rollback from other tests, this test starts with a clean state (within its transaction).
}
}
Bad Example:
// @SpringBootTest
// @Testcontainers
public class ItemRepositoryNoCleanupIT {
private static final Logger log = LoggerFactory.getLogger(ItemRepositoryNoCleanupIT.class);
// @Autowired
// private ItemRepository itemRepository;
private static Long sharedItemId; // Bad: Sharing state between tests via static field
// @Test // Assume tests run in unpredictable order
void testA_createItem() {
// Item item = new Item("Shared Item");
// item = itemRepository.save(item);
// sharedItemId = item.getId();
// assertThat(itemRepository.count()).isGreaterThan(0);
log.warn("Conceptual bad test A: Creates data that might affect other tests.");
}
// @Test
void testB_checkIfItemExists() {
// Bad: This test's success depends on testA_createItem() having run first
// and no cleanup being performed. This leads to flaky and order-dependent tests.
// if (sharedItemId != null) {
// Optional<Item> item = itemRepository.findById(sharedItemId);
// assertThat(item).isPresent();
// } else {
// List<Item> items = itemRepository.findAll();
// assertThat(items.stream().anyMatch(i -> i.getName().equals("Shared Item"))).isTrue(); // Brittle check
// }
log.warn("Conceptual bad test B: Depends on data from another test due to no cleanup.");
}
}
Rule 5: Maintain Clear Test Structure and Assertions
Title: Structure Integration Tests Clearly and Use Specific Assertions
Description:
- Keep integration tests focused on a single user story, API endpoint interaction, or component integration scenario.
- Use descriptive test method names (e.g., should_ExpectedBehavior_when_StateUnderTest
) or JUnit 5's @DisplayName
annotation to clearly explain the scenario being tested.
- Assertions should be specific and provide clear failure messages.
- TestRestTemplate: Use AssertJ assertions for clear validation of response status, headers, and body content (e.g., assertThat(response.getBody().fieldName).isEqualTo(expectedValue)
).
- Database State: Use repositories or JDBC to fetch data after the action and assert its state using libraries like AssertJ for fluent and readable assertions.
- For debugging, consider logging response details in test methods during development, but remove verbose logging in committed code to keep test output clean and focus on assertion failures.
Good example:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
class UserDto {
public Long id;
public String username;
public String email;
public UserDto() {}
public UserDto(String username, String email) { this.username = username; this.email = email; }
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserRegistrationIT {
private static final Logger log = LoggerFactory.getLogger(UserRegistrationIT.class);
// @Autowired private UserRepository userRepository;
// @Autowired private TestRestTemplate restTemplate;
@Test
@DisplayName("POST /users with valid data should create user, return 201, and user details")
void postUsers_withValidData_shouldCreateUserAndReturn201() {
// Arrange: Prepare request data
// UserDto newUser = new UserDto("testuser", "test@example.com");
// HttpHeaders headers = new HttpHeaders();
// headers.setContentType(MediaType.APPLICATION_JSON);
// HttpEntity<UserDto> request = new HttpEntity<>(newUser, headers);
// Act: Send POST request
// ResponseEntity<UserDto> response = restTemplate.postForEntity("/users", request, UserDto.class);
// Assert: Validate API response
// assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
// assertThat(response.getBody()).isNotNull();
// assertThat(response.getBody().username).isEqualTo("testuser");
// assertThat(response.getBody().email).isEqualTo("test@example.com");
// assertThat(response.getBody().id).isNotNull();
// Verify database state (using AssertJ for fluent assertions)
// Long newUserId = response.getBody().id;
// Optional<UserEntity> createdUser = userRepository.findById(newUserId);
// assertThat(createdUser).isPresent();
// assertThat(createdUser.get().getEmail()).isEqualTo("test@example.com");
log.info("Conceptual good test: Clear name, focused scope, specific assertions.");
}
}
Bad Example:
// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class VagueUserActionsIT {
private static final Logger log = LoggerFactory.getLogger(VagueUserActionsIT.class);
// @Autowired private TestRestTemplate restTemplate;
// @Test
void testUserActions() { // Bad: Vague test name, unclear scope
// This test might try to do too many things:
// 1. Create a user
// restTemplate.postForEntity("/users", userDto, UserDto.class); // No assertions
// 2. Update the user
// restTemplate.put("/users/1", updatedUserDto); // No return value validation
// 3. Fetch the user and verify all fields
// ResponseEntity<String> response = restTemplate.getForEntity("/users/1", String.class);
// String responseBody = response.getBody();
// Bad: Asserting a large string is brittle.
// assertThat(responseBody).isEqualTo("{ \"id\":1, \"name\":\"updated\", ... very_long_json ... }"); // Should use specific field assertions instead
// 4. Delete the user
// restTemplate.delete("/users/1"); // No status code validation
log.warn("Conceptual bad test: Vague name, too broad, brittle assertions.");
// Problem: If one part fails, it's hard to know which interaction broke.
// Assertions are not specific enough or are too brittle.
}
}
Rule 6: Optimize for Performance and Ensure Proper Cleanup
Title: Be Mindful of Integration Test Performance and Resource Cleanup
Description:
- Be mindful of integration test execution time. Container startup is often the main overhead.
- Prefer static @Container
fields: This reuses the same container for all tests within a class, significantly speeding up test suites.
- Consider Singleton Container Pattern: For sharing a container across multiple test classes (more advanced setup, use with caution to maintain isolation if state leaks).
- Ensure Testcontainers resources are stopped and removed after the test suite finishes. The testcontainers-junit-jupiter
extension handles this automatically for containers managed via @Container
. If managing containers manually, ensure stop()
is called in a suitable cleanup hook (e.g., @AfterAll
or a JVM shutdown hook for true singletons).
- Separate integration tests (e.g., *IT.java
or *IntegrationTest.java
) from unit tests (*Test.java
) using naming conventions. Configure build tools (Maven Surefire/Failsafe, Gradle) to run them in different phases or tasks if needed (integration tests often run after the application is packaged).
Good example: (Using static @Container for performance and automatic cleanup by junit-jupiter extension)
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import static org.assertj.core.api.Assertions.assertThat; // For assertion
@Testcontainers
public class MyServiceWithSharedContainerIT {
private static final Logger log = LoggerFactory.getLogger(MyServiceWithSharedContainerIT.class);
// Good: Static container is started once for all tests in this class
@Container
static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine"))
.withExposedPorts(6379);
@BeforeAll
static void beforeAll() {
log.info("Redis container started for suite: {}:{}", redis.getContainerIpAddress(), redis.getMappedPort(6379));
// Setup SUT to use redis.getMappedPort(6379) etc.
}
@Test
void testOperationOne_usesRedis() {
assertThat(redis.isRunning()).isTrue();
// ... test logic interacting with service that uses Redis ...
log.info("Test one with shared Redis.");
}
@Test
void testOperationTwo_usesRedis() {
assertThat(redis.isRunning()).isTrue();
// ... another test logic ...
log.info("Test two with shared Redis.");
}
// @AfterAll // Not strictly needed for @Container, as Testcontainers extension handles stop()
// static void afterAll() {
// log.info("Suite finished, Testcontainers will stop the Redis container.");
// }
}
Bad Example:
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
// @Testcontainers // Annotation might be missing or misused
public class MyServiceWithPerMethodContainerIT {
private static final Logger log = LoggerFactory.getLogger(MyServiceWithPerMethodContainerIT.class);
// Bad: Non-static @Container (or manual management per method) starts a new container for EACH test method.
// This is very slow for multiple tests.
// @Container // If this were not static, it would be per-method if @Testcontainers is on class
private GenericContainer<?> redisPerMethod;
// @BeforeEach // Manual start/stop per method is slow and error-prone
void setUpPerMethod() {
redisPerMethod = new GenericContainer<>(DockerImageName.parse("redis:5-alpine"))
.withExposedPorts(6379);
redisPerMethod.start(); // Manual start
log.info("Redis started for method at port: {}", redisPerMethod.getMappedPort(6379));
}
// @Test
void testA() {
// assertThat(redisPerMethod.isRunning()).isTrue();
log.info("Test A using its own Redis instance.");
}
// @Test
void testB() {
// assertThat(redisPerMethod.isRunning()).isTrue();
log.warn("Test B using its own Redis instance (slow!).");
}
// @AfterEach
void tearDownPerMethod() {
if (redisPerMethod != null) {
redisPerMethod.stop(); // Manual stop needed
log.info("Redis stopped for method.");
}
}
// Problem: Significant performance degradation due to container restart for every test.
// Also, higher risk of resource leaks if stop() is missed or fails.
}
This rule serves as a starting point. Refer to authoritative resources on Java Integration Testing, Testcontainers, and Spring Boot's TestRestTemplate for more in-depth understanding and application. ```