# Testcontainers Spring Boot: pengujian integrasi tanpa kesulitan > Panduan lengkap untuk mengonfigurasi Testcontainers dengan Spring Boot 3.4. PostgreSQL, Redis, dan Kafka dalam kontainer Docker untuk pengujian integrasi yang andal dan dapat direproduksi. - Published: 2026-03-19 - Updated: 2026-05-01 - Author: SharpSkill - Tags: testcontainers, spring boot, integration testing, docker, postgresql - Reading time: 14 min --- Pengujian integrasi merupakan tantangan besar dalam pengembangan aplikasi Spring Boot. Pengujian terhadap database PostgreSQL nyata atau broker Kafka memerlukan pemeliharaan infrastruktur yang berat. Testcontainers menyelesaikan masalah ini dengan menjalankan kontainer Docker sesuai permintaan selama pengujian, menjamin lingkungan yang terisolasi dan dapat direproduksi. > **Spring Boot 3.4 dan Testcontainers** > > Spring Boot 3.4 menyertakan dukungan native Testcontainers dengan auto-konfigurasi. Dependensi `spring-boot-testcontainers` menyederhanakan pemasangan secara drastis dan memungkinkan penggunaan ulang kontainer di antara pengujian. ## Memahami integrasi antara Testcontainers dan Spring Boot Testcontainers menyediakan API Java untuk menjalankan kontainer Docker selama eksekusi pengujian. Alih-alih melakukan mock pada dependensi eksternal atau memelihara database pengujian bersama, setiap eksekusi pengujian mendapatkan instance terisolasinya sendiri. Arsitekturnya bergantung pada tiga komponen utama: pustaka Testcontainers yang mengendalikan Docker, modul khusus untuk setiap teknologi (PostgreSQL, Redis, Kafka), dan integrasi Spring Boot yang secara otomatis menyuntikkan parameter koneksi. ```xml org.springframework.boot spring-boot-testcontainers test org.testcontainers postgresql test org.testcontainers junit-jupiter test org.testcontainers testcontainers-bom 1.20.4 pom import ``` BOM Testcontainers memastikan konsistensi versi di antara semua modul yang digunakan dalam proyek. ## Konfigurasi dasar dengan PostgreSQL Kasus penggunaan paling umum melibatkan pengujian dengan database PostgreSQL nyata. Spring Boot 3.4 menawarkan dua pendekatan: anotasi `@ServiceConnection` untuk auto-konfigurasi atau konfigurasi manual melalui `@DynamicPropertySource`. ```java // UserRepositoryIntegrationTest.java @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 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 found = userRepository.findByEmail("search@example.com"); // Then: user is found assertThat(found).isPresent(); assertThat(found.get().getName()).isEqualTo("Search User"); } } ``` Anotasi `@ServiceConnection` secara otomatis mendeteksi tipe kontainer dan mengonfigurasi properti Spring yang sesuai (`spring.datasource.url`, `spring.datasource.username`, dan sebagainya). Pendekatan ini menghilangkan kode konfigurasi yang berulang. > **Siklus hidup kontainer** > > Dengan `@Container` pada bidang statis, kontainer dimulai sekali sebelum semua pengujian dalam kelas dan berhenti setelah pengujian terakhir. Untuk satu kontainer per pengujian, gunakan bidang instance non-statis. ## Konfigurasi manual dengan @DynamicPropertySource Beberapa skenario memerlukan kontrol yang lebih halus atas properti yang disuntikkan. Anotasi `@DynamicPropertySource` memungkinkan pendefinisian nilai konfigurasi secara eksplisit. ```java // OrderRepositoryIntegrationTest.java @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); } } ``` Skrip inisialisasi `withInitScript` mempersiapkan skema atau menyisipkan data referensi sebelum pengujian dijalankan. ## Pengujian integrasi Spring Boot lengkap Untuk menguji seluruh aplikasi dengan semua komponen dimuat, `@SpringBootTest` menggantikan `@DataJpaTest`. Konfigurasi ini menjalankan konteks Spring lengkap bersama kontainer PostgreSQL. ```java // UserServiceIntegrationTest.java @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 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 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` yang dikonfigurasi otomatis mengarah ke server yang dijalankan pada port acak, sehingga menghindari konflik port di antara pengujian paralel. ## Penggunaan ulang kontainer di antara pengujian Menjalankan kontainer Docker memerlukan beberapa detik. Untuk mempercepat eksekusi pengujian, Spring Boot 3.4 memungkinkan penggunaan ulang kontainer melalui konfigurasi terpusat. ```java // TestcontainersConfiguration.java @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"); } } ``` Pengujian mengimpor konfigurasi ini untuk berbagi kontainer yang sama. ```java // ProductServiceIntegrationTest.java @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); } } ``` Untuk mengaktifkan penggunaan ulang kontainer, konfigurasi harus ditambahkan dalam `~/.testcontainers.properties`: ```properties # ~/.testcontainers.properties testcontainers.reuse.enable=true ``` > **Pembersihan data** > > Dengan penggunaan ulang kontainer, data tetap ada di antara eksekusi. Sebaiknya gunakan `@Sql` atau `@BeforeEach` untuk membersihkan tabel sebelum setiap pengujian, atau konfigurasikan skema yang berbeda per kelas pengujian. ## Pengujian dengan Redis dan cache terdistribusi Testcontainers mendukung Redis untuk menguji fitur cache. Modul Redis menyediakan kontainer yang sudah dikonfigurasi sebelumnya dan siap digunakan. ```java // CacheServiceIntegrationTest.java @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 secara otomatis mendeteksi `RedisContainer` melalui `@ServiceConnection` dan mengonfigurasi `spring.data.redis.host` serta `spring.data.redis.port`. ## Pengujian dengan Kafka dan pesan asinkron Aplikasi berbasis event memerlukan pengujian dengan broker Kafka nyata. Testcontainers menyediakan modul Kafka yang menjalankan klaster satu node yang sesuai untuk pengujian. ```java // OrderEventIntegrationTest.java @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 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"); }); } } ``` ```java // OrderEventConsumer.java @Component public class OrderEventConsumer { private final List receivedEvents = new CopyOnWriteArrayList<>(); @KafkaListener(topics = "order-events", groupId = "test-group") public void consume(OrderEvent event) { receivedEvents.add(event); } public List getReceivedEvents() { return List.copyOf(receivedEvents); } public void clear() { receivedEvents.clear(); } } ``` Pustaka Awaitility menangani assertion asinkron dengan timeout, menghindari pemanggilan `Thread.sleep` yang rapuh dalam pengujian. ## Konfigurasi multi-kontainer dengan Docker Compose Untuk aplikasi kompleks yang memerlukan beberapa layanan saling bergantung, Testcontainers mendukung berkas 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" ``` ```java // FullStackIntegrationTest.java @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); } } ``` ## Praktik terbaik dan optimalisasi performa Eksekusi Testcontainers yang efisien memerlukan beberapa optimalisasi untuk mengurangi waktu build. ```java // AbstractIntegrationTest.java @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"); } } ``` ```java // UserIntegrationTest.java 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(); } } ``` Kelas abstrak memusatkan konfigurasi kontainer dan pembersihan database, sehingga menghindari duplikasi kode. ```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 yang ringan** > > Gambar Alpine (`postgres:16-alpine`, `redis:7-alpine`) lebih disukai karena lebih ringan dan lebih cepat dimulai. Untuk pengujian, perbedaan fungsional dengan image lengkap dapat diabaikan. ## Pengujian migrasi database Testcontainers unggul dalam menguji migrasi Flyway atau Liquibase terhadap database nyata. ```java // FlywayMigrationTest.java @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 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> 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"); } } ``` Pengujian ini menjamin migrasi SQL berfungsi dengan benar sebelum penerapan ke produksi. ## Kesimpulan Testcontainers mengubah pengujian integrasi Spring Boot dengan membuatnya andal, dapat direproduksi, dan independen dari lingkungan lokal. Dukungan native Spring Boot 3.4 dengan `@ServiceConnection` menyederhanakan konfigurasi secara signifikan, sementara penggunaan ulang kontainer mengoptimalkan waktu eksekusi. **Daftar Periksa Testcontainers Spring Boot:** - ✅ Gunakan `spring-boot-testcontainers` untuk auto-konfigurasi native - ✅ Utamakan `@ServiceConnection` daripada `@DynamicPropertySource` jika memungkinkan - ✅ Aktifkan `withReuse(true)` untuk mempercepat eksekusi berurutan - ✅ Pusatkan konfigurasi dalam kelas abstrak atau `@TestConfiguration` - ✅ Bersihkan data di antara pengujian dengan `@BeforeEach` atau `@Sql` - ✅ Gunakan image Alpine untuk waktu mulai yang lebih cepat - ✅ Uji migrasi Flyway/Liquibase terhadap PostgreSQL nyata - ✅ Manfaatkan Docker Compose untuk lingkungan multi-layanan --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/spring-boot/testcontainers-spring-boot-integration-testing