Spring Boot 3.2 + Testcontainers: Reliable Integration Testing with Real Dependencies
Without Testcontainers, your integration tests might pass locally with H2 but fail in CI when connecting to a mismatched PostgreSQL version. Production database migrations succeed in development but break when applied to a different database engine.
Prerequisites
- Java 17 or later
- Spring Boot 3.2.x
- Docker Engine 24.0+ with Docker Compose
- JUnit Jupiter 5.9+
- Testcontainers 1.19.3
Configuring Testcontainers for Database Testing
Testcontainers provides disposable database instances for integration tests. Add these dependencies to your pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode
Disable the default datasource in test configuration:
# src/test/resources/application.properties
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:postgresql:15-alpine:///test?TC_DAEMON=true
spring.flyway.enabled=false
Enter fullscreen mode Exit fullscreen mode
Writing a Containerized Integration Test
This test verifies database schema initialization using a real PostgreSQL instance. The container starts before tests and destroys itself afterward:
package com.example.orderservice;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
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;
@Testcontainers
@SpringBootTest
class OrderRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void should_connect_to_live_database() {
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
assertThat(result).isEqualTo(1);
}
}
Enter fullscreen mode Exit fullscreen mode
Run the test with Docker Desktop active. Testcontainers will download the PostgreSQL image on first execution and reuse it for subsequent runs.
Managing Container Lifecycles
Define a base test class to share containers across multiple test classes:
package com.example.shared;
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;
@Testcontainers
public abstract class BaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withReuse(true);
@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);
}
}
Enter fullscreen mode Exit fullscreen mode
Common Mistakes
Mistake 1: Non-static container field causing multiple instances
@Container // Missing static modifier
PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
Enter fullscreen mode Exit fullscreen mode
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
Enter fullscreen mode Exit fullscreen mode
Testcontainers requires static fields for class-level containers. Non-static fields create new containers per test method.
Mistake 2: Hardcoded JDBC URL in test properties
spring.datasource.url=jdbc:postgresql://localhost:5432/test
Enter fullscreen mode Exit fullscreen mode
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
Enter fullscreen mode Exit fullscreen mode
Hardcoded URLs bypass container networking. Use @DynamicPropertySource to inject runtime connection details.
Mistake 3: Missing container reuse configuration
new PostgreSQLContainer<>("postgres:15-alpine");
Enter fullscreen mode Exit fullscreen mode
new PostgreSQLContainer<>("postgres:15-alpine")
.withReuse(true);
Enter fullscreen mode Exit fullscreen mode
Without reuse, Testcontainers destroys containers after each test class. Enable reuse in .testcontainers.properties and chain withReuse(true).
Summary
- Replace in-memory databases with Testcontainers-managed PostgreSQL instances using
@Testcontainersand@Container - Inject dynamic connection properties using
@DynamicPropertySourceregistry - Enable container reuse with
.withReuse(true)andtestcontainers.reuse.enable=truein.testcontainers.properties
The author publishes Spring Boot starter templates at https://gumroad.com
답글 남기기