Testcontainers Spring Boot: āļāļēāļĢāļ—āļ”āļŠāļ­āļš Integration āļ—āļĩāđˆāđ„āļĢāđ‰āļ„āļ§āļēāļĄāļĒāļļāđˆāļ‡āļĒāļēāļ

āļ„āļđāđˆāļĄāļ·āļ­āļ‰āļšāļąāļšāļŠāļĄāļšāļđāļĢāļ“āđŒāļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē Testcontainers āļĢāđˆāļ§āļĄāļāļąāļš Spring Boot 3.4 PostgreSQL, Redis āđāļĨāļ° Kafka āđƒāļ™āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒ Docker āđ€āļžāļ·āđˆāļ­āļāļēāļĢāļ—āļ”āļŠāļ­āļš Integration āļ—āļĩāđˆāđ€āļŠāļ·āđˆāļ­āļ–āļ·āļ­āđ„āļ”āđ‰āđāļĨāļ°āļ—āļģāļ‹āđ‰āļģāđ„āļ”āđ‰

āļāļēāļĢāļ—āļ”āļŠāļ­āļš Integration āļ‚āļ­āļ‡ Spring Boot āļ”āđ‰āļ§āļĒ Testcontainers, PostgreSQL, Redis āđāļĨāļ° Kafka

āļāļēāļĢāļ—āļ”āļŠāļ­āļš Integration āđ€āļ›āđ‡āļ™āļ„āļ§āļēāļĄāļ—āđ‰āļēāļ—āļēāļĒāļŠāļģāļ„āļąāļāđƒāļ™āļāļēāļĢāļžāļąāļ’āļ™āļēāđāļ­āļ›āļžāļĨāļīāđ€āļ„āļŠāļąāļ™ Spring Boot āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļāļąāļšāļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨ PostgreSQL āļˆāļĢāļīāļ‡āļŦāļĢāļ·āļ­ Kafka broker āļ•āđ‰āļ­āļ‡āđƒāļŠāđ‰āđ‚āļ„āļĢāļ‡āļŠāļĢāđ‰āļēāļ‡āļžāļ·āđ‰āļ™āļāļēāļ™āļ—āļĩāđˆāļŦāļ™āļąāļāļŦāļ™āđˆāļ§āļ‡āđƒāļ™āļāļēāļĢāļšāļģāļĢāļļāļ‡āļĢāļąāļāļĐāļē Testcontainers āđāļāđ‰āļ›āļąāļāļŦāļēāļ™āļĩāđ‰āļ”āđ‰āļ§āļĒāļāļēāļĢāļĢāļąāļ™āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒ Docker āļ•āļēāļĄāļ•āđ‰āļ­āļ‡āļāļēāļĢāļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļš āļĢāļąāļšāļ›āļĢāļ°āļāļąāļ™āļŠāļ āļēāļžāđāļ§āļ”āļĨāđ‰āļ­āļĄāļ—āļĩāđˆāđāļĒāļāļ­āļīāļŠāļĢāļ°āđāļĨāļ°āļ—āļģāļ‹āđ‰āļģāđ„āļ”āđ‰

Spring Boot 3.4 āđāļĨāļ° Testcontainers

Spring Boot 3.4 āļĄāļĩāļāļēāļĢāļĢāļ­āļ‡āļĢāļąāļš Testcontainers āđāļšāļšāđ€āļ™āļ—āļĩāļŸāļžāļĢāđ‰āļ­āļĄāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī Dependency spring-boot-testcontainers āļ—āļģāđƒāļŦāđ‰āļāļēāļĢāļ•āļīāļ”āļ•āļąāđ‰āļ‡āļ‡āđˆāļēāļĒāļ‚āļķāđ‰āļ™āļ­āļĒāđˆāļēāļ‡āļĄāļēāļāđāļĨāļ°āļĢāļ­āļ‡āļĢāļąāļšāļāļēāļĢāļ™āļģāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļāļĨāļąāļšāļĄāļēāđƒāļŠāđ‰āđƒāļŦāļĄāđˆāļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļš

āļ—āļģāļ„āļ§āļēāļĄāđ€āļ‚āđ‰āļēāđƒāļˆāļāļēāļĢāļœāļŠāļēāļ™āļĢāļ°āļŦāļ§āđˆāļēāļ‡ Testcontainers āđāļĨāļ° Spring Boot

Testcontainers āļˆāļąāļ”āđ€āļ•āļĢāļĩāļĒāļĄ Java API āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļĢāļąāļ™āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒ Docker āļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļš āđāļ—āļ™āļ—āļĩāđˆāļˆāļ° mock āļāļēāļĢāļžāļķāđˆāļ‡āļžāļēāļ āļēāļĒāļ™āļ­āļāļŦāļĢāļ·āļ­āļšāļģāļĢāļļāļ‡āļĢāļąāļāļĐāļēāļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļ—āļ”āļŠāļ­āļšāļ—āļĩāđˆāđƒāļŠāđ‰āļĢāđˆāļ§āļĄāļāļąāļ™ āļāļēāļĢāļ—āļ”āļŠāļ­āļšāđāļ•āđˆāļĨāļ°āļ„āļĢāļąāđ‰āļ‡āļˆāļ°āđ„āļ”āđ‰āļĢāļąāļšāļ­āļīāļ™āļŠāđāļ•āļ™āļ‹āđŒāļ—āļĩāđˆāđāļĒāļāļ­āļīāļŠāļĢāļ°āļ‚āļ­āļ‡āļ•āļąāļ§āđ€āļ­āļ‡

āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāļ›āļĢāļ°āļāļ­āļšāļ”āđ‰āļ§āļĒāļŠāļēāļĄāļŠāđˆāļ§āļ™āļ›āļĢāļ°āļāļ­āļšāļŦāļĨāļąāļ āđ„āļ”āđ‰āđāļāđˆ āđ„āļĨāļšāļĢāļēāļĢāļĩ Testcontainers āļ—āļĩāđˆāļ„āļ§āļšāļ„āļļāļĄ Docker āđ‚āļĄāļ”āļđāļĨāđ€āļ‰āļžāļēāļ°āļŠāļģāļŦāļĢāļąāļšāđāļ•āđˆāļĨāļ°āđ€āļ—āļ„āđ‚āļ™āđ‚āļĨāļĒāļĩ (PostgreSQL, Redis, Kafka) āđāļĨāļ°āļāļēāļĢāļœāļŠāļēāļ™āļāļąāļš Spring Boot āļ—āļĩāđˆāļ‰āļĩāļ”āļžāļēāļĢāļēāļĄāļīāđ€āļ•āļ­āļĢāđŒāļāļēāļĢāđ€āļŠāļ·āđˆāļ­āļĄāļ•āđˆāļ­āđ‚āļ”āļĒāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī

xml
<!-- pom.xml -->
<dependencies>
    <!-- Main Testcontainers dependency -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-testcontainers</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- PostgreSQL module -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- JUnit 5 support -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <!-- Testcontainers BOM for version management -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-bom</artifactId>
            <version>1.20.4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

BOM āļ‚āļ­āļ‡ Testcontainers āļŠāđˆāļ§āļĒāđƒāļŦāđ‰āđāļ™āđˆāđƒāļˆāļ§āđˆāļēāđ€āļ§āļ­āļĢāđŒāļŠāļąāļ™āļŠāļ­āļ”āļ„āļĨāđ‰āļ­āļ‡āļāļąāļ™āđƒāļ™āļ—āļļāļāđ‚āļĄāļ”āļđāļĨāļ—āļĩāđˆāđƒāļŠāđ‰āđƒāļ™āđ‚āļ›āļĢāđ€āļˆāļāļ•āđŒ

āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļžāļ·āđ‰āļ™āļāļēāļ™āļ”āđ‰āļ§āļĒ PostgreSQL

āļāļĢāļ“āļĩāļāļēāļĢāđƒāļŠāđ‰āļ‡āļēāļ™āļ—āļĩāđˆāļžāļšāļšāđˆāļ­āļĒāļ—āļĩāđˆāļŠāļļāļ”āļ„āļ·āļ­āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļāļąāļšāļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨ PostgreSQL āļˆāļĢāļīāļ‡ Spring Boot 3.4 āļĄāļĩāļŠāļ­āļ‡āđāļ™āļ§āļ—āļēāļ‡ āđ„āļ”āđ‰āđāļāđˆ annotation @ServiceConnection āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī āļŦāļĢāļ·āļ­āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ”āđ‰āļ§āļĒāļ•āļ™āđ€āļ­āļ‡āļœāđˆāļēāļ™ @DynamicPropertySource

UserRepositoryIntegrationTest.javajava
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE)
class UserRepositoryIntegrationTest {

    // Starts a PostgreSQL container before tests
    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        DockerImageName.parse("postgres:16-alpine")
    );

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveAndRetrieveUser() {
        // Given: a user to persist
        User user = new User();
        user.setEmail("test@example.com");
        user.setName("Test User");

        // When: save and retrieve
        User saved = userRepository.save(user);
        Optional<User> found = userRepository.findById(saved.getId());

        // Then: user is correctly persisted
        assertThat(found).isPresent();
        assertThat(found.get().getEmail()).isEqualTo("test@example.com");
    }

    @Test
    void shouldFindUserByEmail() {
        // Given: a user in database
        User user = new User();
        user.setEmail("search@example.com");
        user.setName("Search User");
        userRepository.save(user);

        // When: search by email
        Optional<User> found = userRepository.findByEmail("search@example.com");

        // Then: user is found
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Search User");
    }
}

Annotation @ServiceConnection āļ•āļĢāļ§āļˆāļˆāļąāļšāļ›āļĢāļ°āđ€āļ āļ—āļ‚āļ­āļ‡āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāđ‚āļ”āļĒāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļīāđāļĨāļ°āļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ„āļļāļ“āļŠāļĄāļšāļąāļ•āļīāļ‚āļ­āļ‡ Spring āļ—āļĩāđˆāļŠāļ­āļ”āļ„āļĨāđ‰āļ­āļ‡āļāļąāļ™ (spring.datasource.url, spring.datasource.username āļŊāļĨāļŊ) āđāļ™āļ§āļ—āļēāļ‡āļ™āļĩāđ‰āļŠāđˆāļ§āļĒāļĨāļ”āđ‚āļ„āđ‰āļ”āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ‹āđ‰āļģāļ‹āđ‰āļ­āļ™

āļ§āļ‡āļˆāļĢāļŠāļĩāļ§āļīāļ•āļ‚āļ­āļ‡āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒ

āđ€āļĄāļ·āđˆāļ­āđƒāļŠāđ‰ @Container āļšāļ™ field āđāļšāļš static āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļˆāļ°āđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āđ€āļžāļĩāļĒāļ‡āļ„āļĢāļąāđ‰āļ‡āđ€āļ”āļĩāļĒāļ§āļāđˆāļ­āļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ—āļąāđ‰āļ‡āļŦāļĄāļ”āđƒāļ™āļ„āļĨāļēāļŠāđāļĨāļ°āļŦāļĒāļļāļ”āļŦāļĨāļąāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļŠāļļāļ”āļ—āđ‰āļēāļĒ āļŦāļēāļāļ•āđ‰āļ­āļ‡āļāļēāļĢāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļŦāļ™āļķāđˆāļ‡āļ•āļąāļ§āļ•āđˆāļ­āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļŦāļ™āļķāđˆāļ‡āļ„āļĢāļąāđ‰āļ‡ āļ„āļ§āļĢāđƒāļŠāđ‰ field āđāļšāļš instance āļ—āļĩāđˆāđ„āļĄāđˆāđƒāļŠāđˆ static

āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ”āđ‰āļ§āļĒāļ•āļ™āđ€āļ­āļ‡āļ”āđ‰āļ§āļĒ @DynamicPropertySource

āļšāļēāļ‡āļŠāļ–āļēāļ™āļāļēāļĢāļ“āđŒāļ•āđ‰āļ­āļ‡āļāļēāļĢāļāļēāļĢāļ„āļ§āļšāļ„āļļāļĄāļ—āļĩāđˆāļĨāļ°āđ€āļ­āļĩāļĒāļ”āļāļ§āđˆāļēāđƒāļ™āļāļēāļĢāļ‰āļĩāļ”āļ„āļļāļ“āļŠāļĄāļšāļąāļ•āļī Annotation @DynamicPropertySource āļŠāđˆāļ§āļĒāđƒāļŦāđ‰āļŠāļēāļĄāļēāļĢāļ–āļāļģāļŦāļ™āļ”āļ„āđˆāļēāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđ„āļ”āđ‰āļ­āļĒāđˆāļēāļ‡āļŠāļąāļ”āđ€āļˆāļ™

OrderRepositoryIntegrationTest.javajava
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE)
class OrderRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        DockerImageName.parse("postgres:16-alpine")
    )
        // Specific container configuration
        .withDatabaseName("orders_test")
        .withUsername("test_user")
        .withPassword("test_password")
        // SQL initialization script
        .withInitScript("db/init-orders.sql");

    // Manual injection of dynamic properties
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // JDBC URL generated dynamically with mapped port
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        // Additional properties if needed
        registry.add("spring.jpa.hibernate.ddl-auto", () -> "validate");
    }

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private EntityManager entityManager;

    @Test
    void shouldPersistOrderWithItems() {
        // Given: an order with items
        Order order = new Order();
        order.setOrderNumber("ORD-2026-001");
        order.setStatus(OrderStatus.PENDING);

        OrderItem item = new OrderItem();
        item.setProductId(1L);
        item.setQuantity(2);
        item.setUnitPrice(BigDecimal.valueOf(29.99));
        order.addItem(item);

        // When: save the order
        Order saved = orderRepository.save(order);
        entityManager.flush();
        entityManager.clear();

        // Then: order and its items are persisted
        Order found = orderRepository.findById(saved.getId()).orElseThrow();
        assertThat(found.getItems()).hasSize(1);
        assertThat(found.getItems().get(0).getQuantity()).isEqualTo(2);
    }
}

āļŠāļ„āļĢāļīāļ›āļ•āđŒāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™ withInitScript āđ€āļ•āļĢāļĩāļĒāļĄ schema āļŦāļĢāļ·āļ­āđāļ—āļĢāļāļ‚āđ‰āļ­āļĄāļđāļĨāļ­āđ‰āļēāļ‡āļ­āļīāļ‡āļāđˆāļ­āļ™āļāļēāļĢāļĢāļąāļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļš

āļāļēāļĢāļ—āļ”āļŠāļ­āļš Integration āđāļšāļšāđ€āļ•āđ‡āļĄāļ‚āļ­āļ‡ Spring Boot

āđ€āļžāļ·āđˆāļ­āļ—āļ”āļŠāļ­āļšāđāļ­āļ›āļžāļĨāļīāđ€āļ„āļŠāļąāļ™āļ—āļąāđ‰āļ‡āļŦāļĄāļ”āļžāļĢāđ‰āļ­āļĄāļŠāđˆāļ§āļ™āļ›āļĢāļ°āļāļ­āļšāļ—āļąāđ‰āļ‡āļŦāļĄāļ”āļ—āļĩāđˆāđ‚āļŦāļĨāļ”āđāļĨāđ‰āļ§ @SpringBootTest āđāļ—āļ™āļ—āļĩāđˆ @DataJpaTest āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ™āļĩāđ‰āđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āļšāļĢāļīāļšāļ— Spring āđāļšāļšāđ€āļ•āđ‡āļĄāļĢāđˆāļ§āļĄāļāļąāļšāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒ PostgreSQL

UserServiceIntegrationTest.javajava
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        DockerImageName.parse("postgres:16-alpine")
    );

    @Autowired
    private UserService userService;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreateUserViaApi() {
        // Given: a creation request
        CreateUserRequest request = new CreateUserRequest(
            "api@example.com",
            "API User",
            "securePassword123"
        );

        // When: call the REST API
        ResponseEntity<UserResponse> response = restTemplate.postForEntity(
            "/api/users",
            request,
            UserResponse.class
        );

        // Then: user is created successfully
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().email()).isEqualTo("api@example.com");
    }

    @Test
    void shouldRetrieveUserById() {
        // Given: an existing user
        UserResponse created = userService.createUser(
            new CreateUserRequest("retrieve@example.com", "Retrieve User", "password")
        );

        // When: retrieve by ID
        ResponseEntity<UserResponse> response = restTemplate.getForEntity(
            "/api/users/" + created.id(),
            UserResponse.class
        );

        // Then: user is returned
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().name()).isEqualTo("Retrieve User");
    }
}

TestRestTemplate āļ—āļĩāđˆāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđ‚āļ”āļĒāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļīāļŠāļĩāđ‰āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒāļ—āļĩāđˆāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āļšāļ™āļžāļ­āļĢāđŒāļ•āđāļšāļšāļŠāļļāđˆāļĄ āļŦāļĨāļĩāļāđ€āļĨāļĩāđˆāļĒāļ‡āļ„āļ§āļēāļĄāļ‚āļąāļ”āđāļĒāđ‰āļ‡āļ‚āļ­āļ‡āļžāļ­āļĢāđŒāļ•āļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļšāđāļšāļšāļ‚āļ™āļēāļ™

āļžāļĢāđ‰āļ­āļĄāļ—āļĩāđˆāļˆāļ°āļžāļīāļŠāļīāļ•āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āđāļĨāđ‰āļ§āļŦāļĢāļ·āļ­āļĒāļąāļ‡āļ„āļĢāļąāļš?

āļāļķāļāļāļ™āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āđāļšāļšāđ‚āļ•āđ‰āļ•āļ­āļš, flashcards āđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āļāļēāļĢāļ™āļģāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļāļĨāļąāļšāļĄāļēāđƒāļŠāđ‰āđƒāļŦāļĄāđˆāļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļš

āļāļēāļĢāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒ Docker āđƒāļŠāđ‰āđ€āļ§āļĨāļēāļŦāļĨāļēāļĒāļ§āļīāļ™āļēāļ—āļĩ āđ€āļžāļ·āđˆāļ­āđ€āļĢāđˆāļ‡āļ„āļ§āļēāļĄāđ€āļĢāđ‡āļ§āļāļēāļĢāļĢāļąāļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļš Spring Boot 3.4 āđƒāļŦāđ‰āļŠāļēāļĄāļēāļĢāļ–āļ™āļģāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļāļĨāļąāļšāļĄāļēāđƒāļŠāđ‰āđƒāļŦāļĄāđˆāļœāđˆāļēāļ™āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđāļšāļšāļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒ

TestcontainersConfiguration.javajava
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {

    // Reusable container bean across all tests
    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"))
            .withReuse(true)
            .withLabel("reuse.UUID", "e06d7a87-7d7d-472e-a047-7c2c6d4b5f7a");
    }

    @Bean
    @ServiceConnection
    RedisContainer redisContainer() {
        return new RedisContainer(DockerImageName.parse("redis:7-alpine"))
            .withReuse(true)
            .withLabel("reuse.UUID", "b3c8f9d2-4a5e-4c8d-9f2a-1b3c5d7e9f0a");
    }
}

āļāļēāļĢāļ—āļ”āļŠāļ­āļš import āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ™āļĩāđ‰āđ€āļžāļ·āđˆāļ­āđāļšāđˆāļ‡āļ›āļąāļ™āļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāđ€āļ”āļĩāļĒāļ§āļāļąāļ™

ProductServiceIntegrationTest.javajava
@SpringBootTest
@Import(TestcontainersConfiguration.class)
class ProductServiceIntegrationTest {

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldCacheProductDetails() {
        // Given: a product in database
        Product product = new Product();
        product.setName("Cached Product");
        product.setPrice(BigDecimal.valueOf(99.99));
        productRepository.save(product);

        // When: two successive calls
        ProductDto first = productService.getProductById(product.getId());
        ProductDto second = productService.getProductById(product.getId());

        // Then: second call uses cache
        assertThat(first).isEqualTo(second);
    }
}

āđ€āļžāļ·āđˆāļ­āđ€āļ›āļīāļ”āđƒāļŠāđ‰āļ‡āļēāļ™āļāļēāļĢāļ™āļģāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļāļĨāļąāļšāļĄāļēāđƒāļŠāđ‰āđƒāļŦāļĄāđˆ āļ•āđ‰āļ­āļ‡āđ€āļžāļīāđˆāļĄāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđƒāļ™ ~/.testcontainers.properties:

properties
# ~/.testcontainers.properties
testcontainers.reuse.enable=true
āļāļēāļĢāļĨāđ‰āļēāļ‡āļ‚āđ‰āļ­āļĄāļđāļĨ

āđ€āļĄāļ·āđˆāļ­āđƒāļŠāđ‰āļāļēāļĢāļ™āļģāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļāļĨāļąāļšāļĄāļēāđƒāļŠāđ‰āđƒāļŦāļĄāđˆ āļ‚āđ‰āļ­āļĄāļđāļĨāļˆāļ°āļ„āļ‡āļ­āļĒāļđāđˆāļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļĢāļąāļ™ āļ„āļ§āļĢāđƒāļŠāđ‰ @Sql āļŦāļĢāļ·āļ­ @BeforeEach āđ€āļžāļ·āđˆāļ­āļĨāđ‰āļēāļ‡āļ•āļēāļĢāļēāļ‡āļāđˆāļ­āļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļšāđāļ•āđˆāļĨāļ°āļ„āļĢāļąāđ‰āļ‡ āļŦāļĢāļ·āļ­āļāļģāļŦāļ™āļ” schema āļ—āļĩāđˆāđāļ•āļāļ•āđˆāļēāļ‡āļŠāļģāļŦāļĢāļąāļšāđāļ•āđˆāļĨāļ°āļ„āļĨāļēāļŠāļ—āļ”āļŠāļ­āļš

āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļāļąāļš Redis āđāļĨāļ° Cache āđāļšāļšāļāļĢāļ°āļˆāļēāļĒ

Testcontainers āļĢāļ­āļ‡āļĢāļąāļš Redis āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ—āļ”āļŠāļ­āļšāļŸāļĩāđ€āļˆāļ­āļĢāđŒāđāļ„āļŠ āđ‚āļĄāļ”āļđāļĨ Redis āļˆāļąāļ”āđ€āļ•āļĢāļĩāļĒāļĄāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļ—āļĩāđˆāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđ„āļ§āđ‰āļĨāđˆāļ§āļ‡āļŦāļ™āđ‰āļēāđāļĨāļ°āļžāļĢāđ‰āļ­āļĄāđƒāļŠāđ‰āļ‡āļēāļ™

CacheServiceIntegrationTest.javajava
@SpringBootTest
@Testcontainers
class CacheServiceIntegrationTest {

    @Container
    @ServiceConnection
    static RedisContainer redis = new RedisContainer(
        DockerImageName.parse("redis:7-alpine")
    );

    @Autowired
    private CacheService cacheService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Test
    void shouldStoreAndRetrieveFromCache() {
        // Given: a value to cache
        String key = "user:123";
        String value = "{\"id\":123,\"name\":\"Cached User\"}";

        // When: store in cache
        cacheService.put(key, value, Duration.ofMinutes(10));

        // Then: value is retrievable
        String cached = cacheService.get(key);
        assertThat(cached).isEqualTo(value);
    }

    @Test
    void shouldExpireAfterTtl() throws InterruptedException {
        // Given: a value with short TTL
        String key = "expiring:key";
        cacheService.put(key, "temporary", Duration.ofSeconds(1));

        // When: wait for expiration
        Thread.sleep(1500);

        // Then: key has expired
        String cached = cacheService.get(key);
        assertThat(cached).isNull();
    }

    @Test
    void shouldIncrementCounter() {
        // Given: a counter key
        String counterKey = "page:views:homepage";

        // When: multiple increments
        Long first = redisTemplate.opsForValue().increment(counterKey);
        Long second = redisTemplate.opsForValue().increment(counterKey);
        Long third = redisTemplate.opsForValue().increment(counterKey);

        // Then: counter increments correctly
        assertThat(first).isEqualTo(1);
        assertThat(second).isEqualTo(2);
        assertThat(third).isEqualTo(3);
    }
}

Spring Boot āļ•āļĢāļ§āļˆāļˆāļąāļš RedisContainer āđ‚āļ”āļĒāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļīāļœāđˆāļēāļ™ @ServiceConnection āđāļĨāļ°āļ•āļąāđ‰āļ‡āļ„āđˆāļē spring.data.redis.host āđāļĨāļ° spring.data.redis.port

āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļāļąāļš Kafka āđāļĨāļ°āļāļēāļĢāļŠāđˆāļ‡āļ‚āđ‰āļ­āļ„āļ§āļēāļĄāđāļšāļšāļ­āļ°āļ‹āļīāļ‡āđ‚āļ„āļĢāļ™āļąāļŠ

āđāļ­āļ›āļžāļĨāļīāđ€āļ„āļŠāļąāļ™āļ—āļĩāđˆāļ‚āļąāļšāđ€āļ„āļĨāļ·āđˆāļ­āļ™āļ”āđ‰āļ§āļĒāļ­āļĩāđ€āļ§āļ™āļ•āđŒāļ•āđ‰āļ­āļ‡āļāļēāļĢāļāļēāļĢāļ—āļ”āļŠāļ­āļšāļāļąāļš Kafka broker āļˆāļĢāļīāļ‡ Testcontainers āļˆāļąāļ”āđ€āļ•āļĢāļĩāļĒāļĄāđ‚āļĄāļ”āļđāļĨ Kafka āļ—āļĩāđˆāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āļ„āļĨāļąāļŠāđ€āļ•āļ­āļĢāđŒāđ‚āļŦāļ™āļ”āđ€āļ”āļĩāļĒāļ§āļ—āļĩāđˆāđ€āļŦāļĄāļēāļ°āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ—āļ”āļŠāļ­āļš

OrderEventIntegrationTest.javajava
@SpringBootTest
@Testcontainers
@EmbeddedKafka(partitions = 1, topics = {"order-events"})
class OrderEventIntegrationTest {

    @Container
    @ServiceConnection
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
    );

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Autowired
    private OrderEventConsumer orderEventConsumer;

    @Test
    void shouldPublishAndConsumeOrderEvent() throws Exception {
        // Given: an order event
        OrderEvent event = new OrderEvent(
            "ORD-2026-100",
            OrderEventType.CREATED,
            LocalDateTime.now()
        );

        // When: publish to Kafka
        kafkaTemplate.send("order-events", event.orderId(), event).get();

        // Then: event is consumed (with timeout)
        await()
            .atMost(Duration.ofSeconds(10))
            .untilAsserted(() -> {
                assertThat(orderEventConsumer.getReceivedEvents())
                    .hasSize(1)
                    .first()
                    .extracting(OrderEvent::orderId)
                    .isEqualTo("ORD-2026-100");
            });
    }
}
OrderEventConsumer.javajava
@Component
public class OrderEventConsumer {

    private final List<OrderEvent> receivedEvents = new CopyOnWriteArrayList<>();

    @KafkaListener(topics = "order-events", groupId = "test-group")
    public void consume(OrderEvent event) {
        receivedEvents.add(event);
    }

    public List<OrderEvent> getReceivedEvents() {
        return List.copyOf(receivedEvents);
    }

    public void clear() {
        receivedEvents.clear();
    }
}

āđ„āļĨāļšāļĢāļēāļĢāļĩ Awaitility āļˆāļąāļ”āļāļēāļĢāļāļēāļĢāļ•āļĢāļ§āļˆāļŠāļ­āļšāđāļšāļšāļ­āļ°āļ‹āļīāļ‡āđ‚āļ„āļĢāļ™āļąāļŠāļ”āđ‰āļ§āļĒ timeout āļŦāļĨāļĩāļāđ€āļĨāļĩāđˆāļĒāļ‡āļāļēāļĢāđ€āļĢāļĩāļĒāļ Thread.sleep āļ—āļĩāđˆāđ€āļ›āļĢāļēāļ°āļšāļēāļ‡āđƒāļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļš

āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļŦāļĨāļēāļĒāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļ”āđ‰āļ§āļĒ Docker Compose

āļŠāļģāļŦāļĢāļąāļšāđāļ­āļ›āļžāļĨāļīāđ€āļ„āļŠāļąāļ™āļ—āļĩāđˆāļ‹āļąāļšāļ‹āđ‰āļ­āļ™āļ‹āļķāđˆāļ‡āļ•āđ‰āļ­āļ‡āļāļēāļĢāļšāļĢāļīāļāļēāļĢāļŦāļĨāļēāļĒāļ­āļĒāđˆāļēāļ‡āļ—āļĩāđˆāļ‚āļķāđ‰āļ™āļ•āđˆāļ­āļāļąāļ™ Testcontainers āļĢāļ­āļ‡āļĢāļąāļšāđ„āļŸāļĨāđŒ Docker Compose

yaml
# src/test/resources/docker-compose-test.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
    ports:
      - "5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379"

  localstack:
    image: localstack/localstack:3.0
    environment:
      SERVICES: s3,sqs
      DEFAULT_REGION: eu-west-1
    ports:
      - "4566"
FullStackIntegrationTest.javajava
@SpringBootTest
@Testcontainers
class FullStackIntegrationTest {

    @Container
    static DockerComposeContainer<?> environment = new DockerComposeContainer<>(
        new File("src/test/resources/docker-compose-test.yml")
    )
        .withExposedService("postgres", 5432)
        .withExposedService("redis", 6379)
        .withExposedService("localstack", 4566)
        .waitingFor("postgres", Wait.forListeningPort())
        .waitingFor("redis", Wait.forListeningPort())
        .waitingFor("localstack", Wait.forLogMessage(".*Ready\\.$", 1));

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // PostgreSQL configuration
        String postgresHost = environment.getServiceHost("postgres", 5432);
        Integer postgresPort = environment.getServicePort("postgres", 5432);
        registry.add("spring.datasource.url",
            () -> "jdbc:postgresql://" + postgresHost + ":" + postgresPort + "/testdb");
        registry.add("spring.datasource.username", () -> "testuser");
        registry.add("spring.datasource.password", () -> "testpass");

        // Redis configuration
        String redisHost = environment.getServiceHost("redis", 6379);
        Integer redisPort = environment.getServicePort("redis", 6379);
        registry.add("spring.data.redis.host", () -> redisHost);
        registry.add("spring.data.redis.port", () -> redisPort);

        // LocalStack S3 configuration
        String localstackHost = environment.getServiceHost("localstack", 4566);
        Integer localstackPort = environment.getServicePort("localstack", 4566);
        registry.add("aws.s3.endpoint",
            () -> "http://" + localstackHost + ":" + localstackPort);
    }

    @Autowired
    private FileStorageService fileStorageService;

    @Test
    void shouldUploadFileToS3() {
        // Given: a file to upload
        byte[] content = "Test file content".getBytes();
        String fileName = "test-file.txt";

        // When: upload to S3 via LocalStack
        String url = fileStorageService.upload(fileName, content);

        // Then: file is accessible
        assertThat(url).contains(fileName);
        byte[] downloaded = fileStorageService.download(fileName);
        assertThat(downloaded).isEqualTo(content);
    }
}

āđ€āļĢāļīāđˆāļĄāļāļķāļāļ‹āđ‰āļ­āļĄāđ€āļĨāļĒ!

āļ—āļ”āļŠāļ­āļšāļ„āļ§āļēāļĄāļĢāļđāđ‰āļ‚āļ­āļ‡āļ„āļļāļ“āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āđāļ™āļ§āļ—āļēāļ‡āļ›āļāļīāļšāļąāļ•āļīāļ—āļĩāđˆāļ”āļĩāđāļĨāļ°āļāļēāļĢāđ€āļžāļīāđˆāļĄāļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļž

āļāļēāļĢāļĢāļąāļ™ Testcontainers āļ­āļĒāđˆāļēāļ‡āļĄāļĩāļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāļ•āđ‰āļ­āļ‡āļāļēāļĢāļāļēāļĢāđ€āļžāļīāđˆāļĄāļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāļšāļēāļ‡āļ­āļĒāđˆāļēāļ‡āđ€āļžāļ·āđˆāļ­āļĨāļ”āđ€āļ§āļĨāļē build

AbstractIntegrationTest.javajava
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
public abstract class AbstractIntegrationTest {

    // Shared container across all inheriting classes
    @Container
    @ServiceConnection
    protected static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        DockerImageName.parse("postgres:16-alpine")
    )
        .withReuse(true);

    @Autowired
    protected JdbcTemplate jdbcTemplate;

    @BeforeEach
    void cleanDatabase() {
        // Clean tables in order to respect FK constraints
        jdbcTemplate.execute("TRUNCATE TABLE order_items CASCADE");
        jdbcTemplate.execute("TRUNCATE TABLE orders CASCADE");
        jdbcTemplate.execute("TRUNCATE TABLE users CASCADE");
    }
}
UserIntegrationTest.javajava
class UserIntegrationTest extends AbstractIntegrationTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateUser() {
        // PostgreSQL container already started via parent class
        User user = new User();
        user.setEmail("inherited@test.com");
        user.setName("Inherited Test");

        User saved = userRepository.save(user);

        assertThat(saved.getId()).isNotNull();
    }
}

āļ„āļĨāļēāļŠāļ™āļēāļĄāļ˜āļĢāļĢāļĄāļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāđāļĨāļ°āļāļēāļĢāļĨāđ‰āļēāļ‡āļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨ āļŦāļĨāļĩāļāđ€āļĨāļĩāđˆāļĒāļ‡āļāļēāļĢāļ—āļģāļ‹āđ‰āļģāļ‚āļ­āļ‡āđ‚āļ„āđ‰āļ”

properties
# src/test/resources/application-test.properties
# Test-specific configuration
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false

# Disable Flyway/Liquibase if using ddl-auto
spring.flyway.enabled=false

# Reduced connection pool for tests
spring.datasource.hikari.maximum-pool-size=5
spring.datasource.hikari.minimum-idle=2
Image Docker āļ‚āļ™āļēāļ”āđ€āļĨāđ‡āļ

āļ„āļ§āļĢāđ€āļĨāļ·āļ­āļāđƒāļŠāđ‰ image āđāļšāļš Alpine (postgres:16-alpine, redis:7-alpine) āļ‹āļķāđˆāļ‡āļĄāļĩāļ‚āļ™āļēāļ”āđ€āļĨāđ‡āļāļāļ§āđˆāļēāđāļĨāļ°āđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āđ€āļĢāđ‡āļ§āļāļ§āđˆāļē āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ—āļ”āļŠāļ­āļš āļ„āļ§āļēāļĄāđāļ•āļāļ•āđˆāļēāļ‡āļ”āđ‰āļēāļ™āļŸāļąāļ‡āļāđŒāļŠāļąāļ™āļˆāļēāļ image āđāļšāļšāđ€āļ•āđ‡āļĄāļ–āļ·āļ­āļ§āđˆāļēāļ™āđ‰āļ­āļĒāļĄāļēāļ

āļāļēāļĢāļ—āļ”āļŠāļ­āļš Migration āļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨ

Testcontainers āđ‚āļ”āļ”āđ€āļ”āđˆāļ™āđƒāļ™āļāļēāļĢāļ—āļ”āļŠāļ­āļš migration āļ‚āļ­āļ‡ Flyway āļŦāļĢāļ·āļ­ Liquibase āļāļąāļšāļāļēāļ™āļ‚āđ‰āļ­āļĄāļđāļĨāļˆāļĢāļīāļ‡

FlywayMigrationTest.javajava
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE)
class FlywayMigrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        DockerImageName.parse("postgres:16-alpine")
    );

    @Autowired
    private Flyway flyway;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    void shouldApplyAllMigrations() {
        // Given: migrations applied at startup

        // When: check state
        MigrationInfoService info = flyway.info();

        // Then: all migrations are applied
        assertThat(info.pending()).isEmpty();
        assertThat(info.applied()).isNotEmpty();
    }

    @Test
    void shouldCreateExpectedTables() {
        // Given: migrations applied

        // When: query system tables
        List<String> tables = jdbcTemplate.queryForList(
            "SELECT table_name FROM information_schema.tables " +
            "WHERE table_schema = 'public' AND table_type = 'BASE TABLE'",
            String.class
        );

        // Then: expected tables exist
        assertThat(tables).contains("users", "orders", "order_items", "products");
    }

    @Test
    void shouldHaveCorrectColumnTypes() {
        // Given: users table created

        // When: verify schema
        List<Map<String, Object>> columns = jdbcTemplate.queryForList(
            "SELECT column_name, data_type, is_nullable " +
            "FROM information_schema.columns " +
            "WHERE table_name = 'users'"
        );

        // Then: columns have correct types
        assertThat(columns)
            .extracting(c -> c.get("column_name"))
            .contains("id", "email", "name", "created_at");
    }
}

āļāļēāļĢāļ—āļ”āļŠāļ­āļšāđ€āļŦāļĨāđˆāļēāļ™āļĩāđ‰āļĢāļąāļšāļ›āļĢāļ°āļāļąāļ™āļ§āđˆāļē migration SQL āļ—āļģāļ‡āļēāļ™āļ­āļĒāđˆāļēāļ‡āļ–āļđāļāļ•āđ‰āļ­āļ‡āļāđˆāļ­āļ™āļāļēāļĢ deploy āđ„āļ›āļĒāļąāļ‡ production

āļšāļ—āļŠāļĢāļļāļ›

Testcontainers āđ€āļ›āļĨāļĩāđˆāļĒāļ™āđāļ›āļĨāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļš Integration āļ‚āļ­āļ‡ Spring Boot āđ‚āļ”āļĒāļ—āļģāđƒāļŦāđ‰āđ€āļŠāļ·āđˆāļ­āļ–āļ·āļ­āđ„āļ”āđ‰ āļ—āļģāļ‹āđ‰āļģāđ„āļ”āđ‰ āđāļĨāļ°āđ€āļ›āđ‡āļ™āļ­āļīāļŠāļĢāļ°āļˆāļēāļāļŠāļ āļēāļžāđāļ§āļ”āļĨāđ‰āļ­āļĄāļ—āđ‰āļ­āļ‡āļ–āļīāđˆāļ™ āļāļēāļĢāļĢāļ­āļ‡āļĢāļąāļšāđāļšāļšāđ€āļ™āļ—āļĩāļŸāļ‚āļ­āļ‡ Spring Boot 3.4 āļ”āđ‰āļ§āļĒ @ServiceConnection āļŠāđˆāļ§āļĒāļĨāļ”āļ„āļ§āļēāļĄāļ‹āļąāļšāļ‹āđ‰āļ­āļ™āđƒāļ™āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđ„āļ”āđ‰āļ­āļĒāđˆāļēāļ‡āļĄāļēāļ āđƒāļ™āļ‚āļ“āļ°āļ—āļĩāđˆāļāļēāļĢāļ™āļģāļ„āļ­āļ™āđ€āļ—āļ™āđ€āļ™āļ­āļĢāđŒāļāļĨāļąāļšāļĄāļēāđƒāļŠāđ‰āđƒāļŦāļĄāđˆāļŠāđˆāļ§āļĒāđ€āļžāļīāđˆāļĄāļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāđ€āļ§āļĨāļēāđƒāļ™āļāļēāļĢāļĢāļąāļ™

āļĢāļēāļĒāļāļēāļĢāļ•āļĢāļ§āļˆāļŠāļ­āļš Testcontainers Spring Boot:

  • ✅ āđƒāļŠāđ‰ spring-boot-testcontainers āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļīāđāļšāļšāđ€āļ™āļ—āļĩāļŸ
  • ✅ āđ€āļĨāļ·āļ­āļāđƒāļŠāđ‰ @ServiceConnection āđāļ—āļ™ @DynamicPropertySource āđ€āļĄāļ·āđˆāļ­āļ—āļģāđ„āļ”āđ‰
  • ✅ āđ€āļ›āļīāļ”āđƒāļŠāđ‰āļ‡āļēāļ™ withReuse(true) āđ€āļžāļ·āđˆāļ­āđ€āļĢāđˆāļ‡āļ„āļ§āļēāļĄāđ€āļĢāđ‡āļ§āļāļēāļĢāļĢāļąāļ™āļ•āđˆāļ­āđ€āļ™āļ·āđˆāļ­āļ‡
  • ✅ āļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒāļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđƒāļ™āļ„āļĨāļēāļŠāļ™āļēāļĄāļ˜āļĢāļĢāļĄāļŦāļĢāļ·āļ­ @TestConfiguration
  • ✅ āļĨāđ‰āļēāļ‡āļ‚āđ‰āļ­āļĄāļđāļĨāļĢāļ°āļŦāļ§āđˆāļēāļ‡āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ”āđ‰āļ§āļĒ @BeforeEach āļŦāļĢāļ·āļ­ @Sql
  • ✅ āđƒāļŠāđ‰ image āđāļšāļš Alpine āđ€āļžāļ·āđˆāļ­āđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āđ€āļĢāđ‡āļ§āļ‚āļķāđ‰āļ™
  • ✅ āļ—āļ”āļŠāļ­āļš migration āļ‚āļ­āļ‡ Flyway/Liquibase āļāļąāļš PostgreSQL āļˆāļĢāļīāļ‡
  • ✅ āđƒāļŠāđ‰ Docker Compose āļŠāļģāļŦāļĢāļąāļšāļŠāļ āļēāļžāđāļ§āļ”āļĨāđ‰āļ­āļĄāļŦāļĨāļēāļĒāļšāļĢāļīāļāļēāļĢ

āđāļ—āđ‡āļ

#testcontainers
#spring boot
#integration testing
#docker
#postgresql

āđāļŠāļĢāđŒ

āļšāļ—āļ„āļ§āļēāļĄāļ—āļĩāđˆāđ€āļāļĩāđˆāļĒāļ§āļ‚āđ‰āļ­āļ‡

Spring Modulith: āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ monolith āđāļšāļšāđ‚āļĄāļ”āļđāļĨāļēāļĢāđŒāļ”āđ‰āļ§āļĒ Spring Boot

Spring Modulith: āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ Monolith āđāļšāļšāđ‚āļĄāļ”āļđāļĨāļēāļĢāđŒ

āđ€āļĢāļĩāļĒāļ™āļĢāļđāđ‰ Spring Modulith āđ€āļžāļ·āđˆāļ­āļŠāļĢāđ‰āļēāļ‡ monolith āđāļšāļšāđ‚āļĄāļ”āļđāļĨāļēāļĢāđŒāđƒāļ™ Java āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ āđ‚āļĄāļ”āļđāļĨ āļ­āļĩāđ€āļ§āļ™āļ•āđŒāļ­āļ°āļ‹āļīāļ‡āđ‚āļ„āļĢāļ™āļąāļŠ āđāļĨāļ°āļāļēāļĢāļ—āļ”āļŠāļ­āļšāļ”āđ‰āļ§āļĒ Spring Boot 3

āļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Batch 5: partitioning, chunk āđāļĨāļ° fault tolerance

āļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Batch 5: Partitioning, Chunk āđāļĨāļ° Fault Tolerance

āđ€āļŠāļĩāđˆāļĒāļ§āļŠāļēāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Batch 5: 15 āļ„āļģāļ–āļēāļĄāļŠāļģāļ„āļąāļāđ€āļāļĩāđˆāļĒāļ§āļāļąāļš partitioning āļāļēāļĢāļ›āļĢāļ°āļĄāļ§āļĨāļœāļĨāđāļšāļš chunk āđāļĨāļ°āļ„āļ§āļēāļĄāļ—āļ™āļ—āļēāļ™āļ•āđˆāļ­āļ‚āđ‰āļ­āļœāļīāļ”āļžāļĨāļēāļ” āļžāļĢāđ‰āļ­āļĄāļ•āļąāļ§āļ­āļĒāđˆāļēāļ‡āđ‚āļ„āđ‰āļ” Java 21

Spring Boot āļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļ˜āļļāļĢāļāļĢāļĢāļĄ: āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđāļĨāļ°āļ•āļąāļ§āļ­āļĒāđˆāļēāļ‡āļˆāļĢāļīāļ‡

āļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot: āļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļ˜āļļāļĢāļāļĢāļĢāļĄ

āđ€āļŠāļĩāđˆāļĒāļ§āļŠāļēāļāļāļēāļĢāļāļĢāļ°āļˆāļēāļĒāļ˜āļļāļĢāļāļĢāļĢāļĄāđƒāļ™ Spring Boot: REQUIRED, REQUIRES_NEW, NESTED āđāļĨāļ°āļ­āļ·āđˆāļ™ āđ† 12 āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāļžāļĢāđ‰āļ­āļĄāđ‚āļ„āđ‰āļ”āđāļĨāļ°āļāļąāļšāļ”āļąāļāļ—āļąāđˆāļ§āđ„āļ›