Skip to content

Spring Boot Local Testing Best Practices

To create a new Cursor Rule:

  1. Enter the name as sprint-boot-local-testing
  2. Copy & paste the file content from below

For more information, visit the Project rules.

---
description: 
globs: 
alwaysApply: false
---
# Spring Boot Local Testing with Docker Compose

Best practices for local testing in Spring Boot applications using `spring-boot-docker-compose` for seamless integration with external services like databases, message queues, and caches.

## Implementing These Principles

These guidelines are built upon the following core principles:

- **Seamless Integration**: Use spring-boot-docker-compose to automatically manage external service dependencies
- **Environment Parity**: Maintain consistency between local development and production environments
- **Test Isolation**: Ensure tests are independent and can run in any order without side effects
- **Performance Optimization**: Minimize container startup time and resource usage for faster development cycles
- **Configuration Management**: Use profiles and dynamic properties for flexible environment-specific configurations

## Table of contents

- Rule 1: Dependency Configuration
- Rule 2: Docker Compose Service Definition
- Rule 3: Application Profile Configuration
- Rule 4: Integration Test Setup
- Rule 5: Service Connection Management
- Rule 6: Health Check Implementation
- Rule 7: Test Data Management
- Rule 8: Performance Optimization

## Rule 1: Dependency Configuration

Title: Proper Spring Boot Docker Compose Dependency Setup
Description: Configure the spring-boot-docker-compose dependency correctly for runtime-only usage to automatically manage Docker services during application startup.

**Good example:**

```xml
<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-docker-compose</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- Testcontainers for integration tests -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>

Bad Example:

<!-- Don't include as compile dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-docker-compose</artifactId>
    <scope>compile</scope>
</dependency>

<!-- Missing testcontainers dependency -->

Rule 2: Docker Compose Service Definition

Title: Well-structured Docker Compose Configuration Description: Define services with proper health checks, environment variables, and port mappings for reliable local testing.

Good example:

# compose.yaml
services:
  postgres:
    image: 'postgres:15'
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
    ports:
      - '5432:5432'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Bad Example:

# compose.yaml
services:
  postgres:
    image: 'postgres'  # No version specified
    environment:
      - POSTGRES_PASSWORD=password  # Hardcoded, no DB/USER
    # Missing health checks
    # Missing volume persistence
    # Missing proper port mapping

Rule 3: Application Profile Configuration

Title: Environment-specific Configuration Management Description: Use Spring profiles to manage different configurations for local development, testing, and production environments.

Good example:

# application-local.yml
spring:
  docker:
    compose:
      enabled: true
      file: compose.yaml
      lifecycle-management: start_and_stop
      readiness:
        wait: HEALTHY
        timeout: 2m

  datasource:
    url: jdbc:postgresql://localhost:5432/testdb
    username: testuser
    password: testpass

  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

logging:
  level:
    org.springframework.boot.docker: DEBUG

Bad Example:

# application.yml
spring:
  docker:
    compose:
      enabled: true  # Always enabled, no profile separation
      lifecycle-management: none  # No lifecycle management

  datasource:
    url: jdbc:postgresql://localhost:5432/proddb  # Production DB in local config
    username: root  # Unsafe credentials
    password: admin

Rule 4: Integration Test Setup

Title: Testcontainers Integration with Service Connections Description: Use @ServiceConnection and @Testcontainers for clean integration test setup with automatic container management.

Good example:

@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class UserRepositoryIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveAndRetrieveUser() {
        User user = new User("john.doe@example.com", "John Doe");
        User saved = userRepository.save(user);

        assertThat(saved.getId()).isNotNull();
        assertThat(userRepository.findByEmail("john.doe@example.com"))
                .isPresent()
                .get()
                .extracting(User::getName)
                .isEqualTo("John Doe");
    }
}

Bad Example:

@SpringBootTest
class UserRepositoryIntegrationTest {

    // No container management
    // No profile specification
    // Assumes external database is running

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveAndRetrieveUser() {
        // Test may fail if database is not available
        // No cleanup between tests
        User user = new User("john.doe@example.com", "John Doe");
        userRepository.save(user);
        // No proper assertions
    }
}

Rule 5: Service Connection Management

Title: Dynamic Service Configuration Description: Use @DynamicPropertySource and @ServiceConnection for flexible service configuration in tests.

Good example:

@TestConfiguration
public class TestContainersConfiguration {

    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:15")
                .withDatabaseName("testdb")
                .withUsername("test")
                .withPassword("test")
                .withReuse(true);  // Reuse container across tests
    }

    @Bean
    @ServiceConnection
    RedisContainer redisContainer() {
        return new RedisContainer("redis:7-alpine")
                .withReuse(true);
    }
}

// Alternative approach with @DynamicPropertySource
@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);
}

Bad Example:

@TestConfiguration
public class TestConfiguration {

    // Hardcoded connection properties
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:postgresql://localhost:5432/testdb");
        dataSource.setUsername("test");
        dataSource.setPassword("test");
        return dataSource;
    }

    // No container lifecycle management
    // No dynamic property configuration
}

Rule 6: Health Check Implementation

Title: Robust Service Health Monitoring Description: Implement proper health checks for all external services to ensure they are ready before running tests.

Good example:

# compose.yaml
services:
  postgres:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

Bad Example:

# compose.yaml
services:
  postgres:
    image: postgres:15
    # No health check - application may start before DB is ready

  redis:
    image: redis:7-alpine
    # No readiness verification

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    # May cause intermittent test failures

Rule 7: Test Data Management

Title: Clean Test Data Handling Description: Ensure proper test data setup and cleanup for reliable and isolated tests.

Good example:

@SpringBootTest
@Testcontainers
@Transactional
@Rollback
class OrderServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Autowired
    private OrderService orderService;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    @Sql(scripts = "/test-data/orders.sql", executionPhase = BEFORE_TEST_METHOD)
    @Sql(scripts = "/test-data/cleanup.sql", executionPhase = AFTER_TEST_METHOD)
    void shouldProcessOrderCorrectly() {
        Order order = orderService.createOrder(createOrderRequest());

        entityManager.flush();
        entityManager.clear();

        Order retrieved = orderService.findById(order.getId());
        assertThat(retrieved.getStatus()).isEqualTo(OrderStatus.PENDING);
    }

    private OrderRequest createOrderRequest() {
        return OrderRequest.builder()
                .customerId(1L)
                .items(List.of(new OrderItem("product-1", 2)))
                .build();
    }
}

Bad Example:

@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldProcessOrderCorrectly() {
        // No data cleanup - may affect other tests
        // No transaction management
        // Hardcoded data that may conflict
        Order order = new Order();
        order.setId(1L);  // Hardcoded ID
        order.setCustomerId(999L);  // May not exist

        orderService.save(order);
        // No proper verification
    }
}

Rule 8: Performance Optimization

Title: Optimized Container and Test Execution Description: Configure container reuse and optimize resource usage for faster development cycles.

Good example:

# application-test.properties
spring.docker.compose.lifecycle-management=start_only
testcontainers.reuse.enable=true

# Resource optimization
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
logging.level.org.hibernate.SQL=WARN
@TestMethodOrder(OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserServicePerformanceTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withReuse(true)
            .withTmpFs(Map.of("/var/lib/postgresql/data", "rw"));

    @BeforeAll
    void setupTestData() {
        // Setup once for all tests
    }

    @AfterAll
    void cleanup() {
        // Cleanup once after all tests
    }
}

Bad Example:

# application-test.properties
spring.docker.compose.lifecycle-management=start_and_stop
# No container reuse - slow test execution

spring.jpa.show-sql=true  # Verbose logging slows down tests
logging.level.org.hibernate=DEBUG
class UserServiceTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    // No reuse configuration
    // No memory optimization

    @BeforeEach
    void setup() {
        // Expensive setup for each test
        populateDatabase();
    }

    @AfterEach
    void cleanup() {
        // Expensive cleanup for each test
        clearDatabase();
    }
}