| 요구 사항 | 버전 | 확인 명령어 |
|---|
| Docker | 최신 안정 버전 | docker --version |
| Java JDK | 8 이상 | java -version |
| Maven/Gradle | 최신 | mvn -v or gradle -v |
<!-- Core TestContainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Integration -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- Database Modules -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
```## 설치
```kotlin
// Kotlin DSL
testImplementation("org.testcontainers:testcontainers:1.19.3")
testImplementation("org.testcontainers:junit-jupiter:1.19.3")
testImplementation("org.testcontainers:postgresql:1.19.3")
testImplementation("org.testcontainers:mysql:1.19.3")
testImplementation("org.testcontainers:kafka:1.19.3")
// Groovy DSL
testImplementation 'org.testcontainers:testcontainers:1.19.3'
testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
testImplementation 'org.testcontainers:postgresql:1.19.3'
```### 전제 조건
| 플랫폼 | 설치 명령어 |
|----------|---------------------|
| Ubuntu/Debian | `curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh` |
| macOS | `brew install --cask docker` |
| Windows | docker.com에서 Docker Desktop 다운로드 |
| Linux Post-Install | `sudo usermod -aG docker $USER && newgrp docker` |### Maven 의존성
| 명령어 | 설명 |
|---------|-------------|
| `container.start()` | 컨테이너 인스턴스 시작하기 |
| `container.stop()` | 컨테이너 중지 및 제거 |
| `container.isRunning()` | 컨테이너가 현재 실행 중인지 확인 |
| `container.getContainerId()` | Docker 컨테이너 ID 가져오기 |
| `container.getDockerImageName()` | 사용된 이미지 이름 가져오기 |
| `container.getLogs()` | 모든 컨테이너 로그를 문자열로 검색 |
| `container.getHost()` | 호스트 주소 가져오기 (보통 localhost) |
| `container.getMappedPort(int)` | 컨테이너 포트에 매핑된 호스트 포트 가져오기 |
| `container.getExposedPorts()` | 노출된 컨테이너 포트 모두 나열 |
| `container.getContainerInfo()` | 컨테이너의 상세 정보 가져오기 |### Gradle 의존성
| 명령어 | 설명 |
|---------|-------------|
| `new GenericContainer("image:tag")` | 모든 Docker 이미지로부터 컨테이너 생성 |
| `.withExposedPorts(port1, port2)` | 컨테이너 포트를 호스트에 노출 |
| `.withEnv("KEY", "value")` | 환경 변수 설정 |
| `.withCommand("cmd", "arg1")` | 컨테이너 명령 재정의 |
| `.withLabel("key", "value")` | 컨테이너에 Docker 레이블 추가 |
| `.withNetworkMode("host")` | 컨테이너 네트워크 모드 설정 |
| `.withPrivilegedMode()` | 특권 모드로 컨테이너 실행 |
| `.withCreateContainerCmdModifier(cmd)` | 컨테이너 생성 명령어 수정 |
| `.withStartupTimeout(Duration)` | 최대 시작 대기 시간 설정 |
| `.withReuse(true)` | 테스트 간 컨테이너 재사용 활성화 |### 플랫폼별 Docker 설정
| 명령어 | 설명 |
|---------|-------------|
| `new PostgreSQLContainer("postgres:15")` | PostgreSQL 컨테이너 생성 |
| `new MySQLContainer("mysql:8.0")` | MySQL 컨테이너 생성 |
| `new MongoDBContainer("mongo:6.0")` | MongoDB 컨테이너 생성 |
| `.withDatabaseName("dbname")` | 데이터베이스 이름 설정 |
| `.withUsername("user")` | 데이터베이스 사용자 이름 설정 |
| `.withPassword("pass")` | 데이터베이스 비밀번호 설정 |
| `.getJdbcUrl()` | JDBC 연결 URL 가져오기 |
| `.getReplicaSetUrl()` | MongoDB 복제 세트 URL 가져오기 (MongoDB) |
| `.withInitScript("init.sql")` | 시작 시 SQL 스크립트 실행 |
| `.withConfigurationOverride("path")` | 데이터베이스 구성 재정의 |## 기본 명령어
| 명령어 | 설명 |
|---------|-------------|
| `.waitingFor(Wait.forListeningPort())` | 포트가 수신 대기할 때까지 기다리세요 |
| `.waitingFor(Wait.forHttp("/health"))` | HTTP 엔드포인트의 응답을 기다리세요 |
| `.waitingFor(Wait.forLogMessage(".*ready.*", 1))` | 특정 로그 메시지를 기다리세요 |
| `.waitingFor(Wait.forHealthcheck())` | Docker 헬스체크가 통과할 때까지 기다리기 |
| `.waitingFor(Wait.forSuccessfulCommand("cmd"))` | 명령이 성공할 때까지 대기 |
| `.forStatusCode(200)` | 예상되는 HTTP 상태 코드 지정 |
| `.forStatusCodeMatching(code -> code < 500)` | 맞춤형 상태 코드 매처 |
| `.withStartupTimeout(Duration.ofMinutes(2))` | 시작 시간 초과 지속 시간 설정 |
| `.withReadTimeout(Duration.ofSeconds(10))` | HTTP 읽기 시간 초과 설정 |
| `.forResponsePredicate(response -> true)` | 사용자 정의 응답 검증 |### 컨테이너 수명 주기 작업
| 명령어 | 설명 |
|---------|-------------|
| `.withFileSystemBind("host", "container")` | 호스트 디렉터리 마운트하기 |
| `.withFileSystemBind(path, target, BindMode.READ_ONLY)` | 특정 모드로 마운트하기 |
| `.withClasspathResourceMapping("res", "path")` | 클래스패스 리소스 마운트 |
| `.copyFileToContainer(MountableFile, "path")` | 파일을 컨테이너에 복사하기 |
| `.withCopyFileToContainer(file, path)` | 시작 시 파일 복사하기 |
| `.withTmpFs(Map.of("/tmp", "rw"))` | tmpfs 파일 시스템 마운트하기 |
| `MountableFile.forClasspathResource("file")` | 참조 클래스패스 파일 |
| `MountableFile.forHostPath("/path")` | 참조 호스트 파일 시스템 경로 |
| `BindMode.READ_WRITE` | 읽기 및 쓰기 액세스 허용 |
| `BindMode.READ_ONLY` | 읽기 전용 접근만 허용 |### 일반 컨테이너 생성
| 명령어 | 설명 |
|---------|-------------|
| `Network.newNetwork()` | 사용자 지정 Docker 네트워크 생성 |
| `.withNetwork(network)` | 컨테이너를 네트워크에 연결 |
| `.withNetworkAliases("alias")` | 컨테이너의 네트워크 별칭 설정 |
| `.dependsOn(otherContainer)` | 컨테이너 종속성 정의 |
| `.withExtraHost("hostname", "ip")` | /etc/hosts에 항목 추가 |
| `.getNetworkAliases()` | 모든 네트워크 별칭 가져오기 |
| `.withAccessToHost(true)` | 호스트에 접근할 수 있도록 컨테이너 허용 |
| `Network.SHARED` | 테스트 간 공유 네트워크 사용 |
| `.withNetworkMode("bridge")` | 특정 네트워크 모드 설정 |
| `.getNetwork()` | 네트워크 컨테이너가 연결된 상태 가져오기 |### 데이터베이스 컨테이너
| 명령어 | 설명 |
|---------|-------------|
| `.execInContainer("cmd", "arg1")` | 실행 중인 컨테이너에서 명령 실행 |
| `.execInContainer(Charset, "cmd")` | 특정 문자셋으로 실행 |
| `result.getStdout()` | 실행의 표준 출력 가져오기 |
| `result.getStderr()` | 실행의 표준 오류 가져오기 |
| `result.getExitCode()` | 명령어 종료 코드 가져오기 |
| `.copyFileFromContainer("path", consumer)` | 컨테이너에서 파일 복사하기 |
| `.followOutput(consumer)` | 스트림 컨테이너 로그 |
| `new Slf4jLogConsumer(logger)` | SLF4J 로거에 로그 출력하기 |
| `new ToStringConsumer()` | 출력을 문자열로 캡처 |
| `new WaitingConsumer()` | 특정 출력을 기다리세요 |## 고급 사용법
| 명령어 | 설명 |
|---------|-------------|
| `@Testcontainers` | TestContainers 확장 기능 활성화 |
| `@Container` | 마크 필드를 컨테이너로 관리하기 |
| `static @Container` | 모든 테스트에서 컨테이너 공유 |
| `@DynamicPropertySource` | 컨테이너 속성 주입 |
| `registry.add("prop", container::getUrl)` | 동적 속성 추가 |
| `@BeforeAll` | 모든 테스트 전에 컨테이너 설정 |
| `@AfterAll` | 모든 테스트 후 컨테이너 정리하기 |
| `@Nested` | 공유 컨테이너로 테스트 그룹화 |
| `Startables.deepStart(containers).join()` | 병렬로 여러 컨테이너 시작하기 |
| `.withReuse(true)` | 테스트 실행 간 컨테이너 재사용 |### 대기 전략
`testcontainers.properties`### 파일 및 볼륨 작업
`src/test/resources`### 네트워킹
```properties
# Docker host configuration
docker.host=unix:///var/run/docker.sock
# docker.host=tcp://localhost:2375
# Container reuse (requires Docker labels support)
testcontainers.reuse.enable=true
# Image pull policy
testcontainers.image.pull.policy=DefaultPullPolicy
# Ryuk container (cleanup)
testcontainers.ryuk.disabled=false
testcontainers.ryuk.container.image=testcontainers/ryuk:0.5.1
# Checks
testcontainers.checks.disable=false
```### 컨테이너 실행
```java
@Container
static DockerComposeContainer environment =
new DockerComposeContainer(new File("docker-compose.yml"))
.withExposedService("db", 5432)
.withExposedService("redis", 6379)
.withLocalCompose(true)
.withPull(true)
.waitingFor("db", Wait.forHealthcheck());
```### JUnit 5 통합
```java
public class CustomPostgreSQLContainer extends PostgreSQLContainer<CustomPostgreSQLContainer> {
private static final String IMAGE_VERSION = "postgres:15-alpine";
public CustomPostgreSQLContainer() {
super(IMAGE_VERSION);
this.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass")
.withInitScript("init.sql")
.withCommand("postgres -c max_connections=200");
}
}
```## 구성
```java
@SpringBootTest
@Testcontainers
class ApplicationTests {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb");
@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");
}
@Test
void contextLoads() {
// Test with real database
}
}
```### TestContainers 속성
```java
@Testcontainers
@SpringBootTest
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withInitScript("schema.sql");
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("john@example.com", "John Doe");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("john@example.com");
assertTrue(found.isPresent());
assertEquals("John Doe", found.get().getName());
}
}
```### 사용 사례 2: Kafka 메시지 처리 테스트
```java
@Testcontainers
class KafkaIntegrationTest {
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
);
@DynamicPropertySource
static void kafkaProperties(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private MessageListener messageListener;
@Test
void shouldProcessMessage() throws Exception {
String topic = "test-topic";
String message = "Hello Kafka!";
kafkaTemplate.send(topic, message);
// Wait for message processing
await().atMost(Duration.ofSeconds(10))
.until(() -> messageListener.getReceivedMessages().size() == 1);
assertEquals(message, messageListener.getReceivedMessages().get(0));
}
}
```### 사용 사례 3: 다중 컨테이너 마이크로서비스 테스트
```java
@Testcontainers
class MicroservicesIntegrationTest {
static Network network = Network.newNetwork();
@Container
static PostgreSQLContainer<?> database = new PostgreSQLContainer<>("postgres:15-alpine")
.withNetwork(network)
.withNetworkAliases("database")
.withDatabaseName("orders");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withNetwork(network)
.withNetworkAliases("redis")
.withExposedPorts(6379);
@Container
static GenericContainer<?> orderService = new GenericContainer<>("order-service:latest")
.withNetwork(network)
.withExposedPorts(8080)
.withEnv("DATABASE_URL", "jdbc:postgresql://database:5432/orders")
.withEnv("REDIS_HOST", "redis")
.dependsOn(database, redis)
.waitingFor(Wait.forHttp("/actuator/health").forStatusCode(200));
@Test
void shouldCreateOrder() {
String baseUrl = "http://" + orderService.getHost() + ":"
+ orderService.getMappedPort(8080);
// Make HTTP request to order service
RestTemplate restTemplate = new RestTemplate();
Order order = new Order("item-123", 2);
ResponseEntity<Order> response = restTemplate.postForEntity(
baseUrl + "/orders", order, Order.class
);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertNotNull(response.getBody().getId());
}
}
```### 사용 사례 4: MongoDB 통합 테스트
```java
@Testcontainers
@DataMongoTest
class ProductRepositoryTest {
@Container
static MongoDBContainer mongodb = new MongoDBContainer("mongo:6.0")
.withExposedPorts(27017);
@DynamicPropertySource
static void mongoProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", mongodb::getReplicaSetUrl);
}
@Autowired
private ProductRepository productRepository;
@Test
void shouldFindProductsByCategory() {
// Insert test data
productRepository.save(new Product("Laptop", "Electronics", 999.99));
productRepository.save(new Product("Mouse", "Electronics", 29.99));
productRepository.save(new Product("Desk", "Furniture", 299.99));
// Query by category
List<Product> electronics = productRepository.findByCategory("Electronics");
assertEquals(2, electronics.size());
assertTrue(electronics.stream()
.allMatch(p -> p.getCategory().equals("Electronics")));
}
}
```### 사용 사례 5: Docker Compose 테스트 환경
```java
@Testcontainers
class FullStackIntegrationTest {
@Container
static DockerComposeContainer<?> environment = new DockerComposeContainer<>(
new File("src/test/resources/docker-compose-test.yml")
)
.withExposedService("api", 8080,
Wait.forHttp("/health").forStatusCode(200))
.withExposedService("postgres", 5432,
Wait.forListeningPort())
.withExposedService("redis", 6379)
.withLocalCompose(true);
@Test
void shouldConnectToAllServices() {
String apiHost = environment.getServiceHost("api", 8080);
Integer apiPort = environment.getServicePort("api", 8080);
String apiUrl = "http://" + apiHost + ":" + apiPort;
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(
apiUrl + "/health", String.class
);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
```## 모범 사례
### 컨테이너 관리
- **테스트 클래스에 정적 컨테이너 사용**: 시작 시간과 리소스 사용을 줄이기 위해 모든 테스트 메서드에서 컨테이너 공유
- **컨테이너 재사용 구현**: 활성화`testcontainers.reuse.enable=true`로컬 개발 테스트 주기 가속화
- **리소스 정리**: `@AfterAll`또는 JUnit 통합 외부에서 수동으로 컨테이너를 관리할 때 try-with-resources 사용
- **특정 이미지 태그 사용**: `latest`태그 방지; 재현 가능한 테스트를 위해 `postgres:15-alpine`같은 특정 버전 사용
### 성능 최적화
- **병렬 컨테이너 시작**: `Startables.deepStart()`를 사용하여 독립적인 여러 컨테이너를 동시에 시작
- **컨테이너 재시작 최소화**: 가능한 경우 메서드 수준 대신 `@Container`클래스 수준 사용
- **경량 이미지 선택**: 풀 시간과 디스크 사용을 줄이기 위해 Alpine 기반 이미지 선호 (예: `postgres:15-alpine`)
- **대기 전략 현명하게 구현**: 불필요한 지연을 방지하면서 컨테이너 준비 상태 보장
### 테스트 전략
- **프로덕션과 유사한 의존성에 대해 테스트**: 임베디드 버전 대신 실제 데이터베이스 엔진 및 메시지 브로커 사용
- **테스트 데이터 격리**: 각 테스트가 자체 데이터를 생성하고 정리하여 테스트 간 종속성 방지
- **네트워크 별칭 사용**: 사용자 정의 네트워크 생성 및 컨테이너 간 통신을 위한 의미 있는 별칭 사용
- **상태 검사 구현**: 테스트 실행 전 컨테이너가 완전히 준비될 때까지 항상 적절한 대기 전략 구성
### CI/CD 통합
- **Docker 가용성 확인**: CI 환경에 Docker가 설치되고 접근 가능한지 확인
- **타임아웃 적절히 구성**: CI 환경 성능을 고려하여 현실적인 시작 타임아웃 설정
- **이미지 캐싱 사용**: CI가 Docker 이미지를 캐시하여 후속 실행 속도 향상
- **리소스 사용 모니터링**: CI 환경의 메모리 및 CPU 제한에 주의; 컨테이너 구성 조정
### 보안 및 유지 관리
- **TestContainers 최신 상태 유지**: 보안 패치 및 새 기능을 위해 최신 버전으로 정기적 업데이트
- **공식 이미지 사용**: 검증된 게시자의 공식 Docker 이미지 선호
- **특권 모드 방지**: 절대적으로 필요한 경우에만 `.withPrivilegedMode()`사용
- **노출된 포트 검토**: 테스트에 실제로 필요한 포트만 노출
## 문제 해결
| 문제 | 솔루션 |
|-------|----------|
| **Container fails to start: "Cannot connect to Docker daemon"** | Ensure Docker is running: `docker ps`. On Linux, add user to docker group: `sudo usermod -aG docker $USER` |
| **Tests timeout during container startup** | Increase timeout: `.withStartupTimeout(Duration.ofMinutes(5))` or check Docker resources (CPU/Memory) |
| **Port binding conflicts: "Address already in use"** | Use dynamic ports: `container.getMappedPort()` instead of fixed ports, or stop conflicting services |
| **Image pull fails: "manifest unknown"** | Verify image name and tag exist on Docker Hub. Use specific tags instead of `latest` |
| **Container starts but tests fail to connect** | Check wait strategy is appropriate: add `.waitingFor(Wait.forListeningPort())` or `.waitingFor(Wait.forHealthcheck())` |
| **Out of memory errors during tests** | Reduce number of parallel containers, increase Docker memory limit in Docker Desktop settings, or use `.withReuse(true)` |
| **"Ryuk container not found" warnings** | Disable Ryuk if needed: set `testcontainers.ryuk.disabled=true` in properties (not recommended for production) |
| **Tests work locally but fail in CI** | CI에 Docker가 설치되어 있는지 확인하고, 타임아웃 설정을 점검하고, 네트워크 연결성을 검증하고, CI 환경의 리소스 제한을 검토하세요 |
| **Database connection refused** | Wait for database to be ready: use `.waitingFor(Wait.forListeningPort())` and verify JDBC URL with `container.getJdbcUrl()` |
| **Container logs show errors but tests pass** | Enable log following: `container.followOutput(new Slf4jLogConsumer(log))` to debug container issues |
| **Slow test execution** | Use container reuse, share containers at class level, start containers in parallel with `Startables.deepStart()` |
| **"No such file or directory" when mounting volumes** | Use absolute paths or `MountableFile.forClasspathResource()` for classpath resources |
| **Network communication between containers fails** | Create custom network: `Network.newNetwork()` and attach containers with `.withNetwork(network)` and `.withNetworkAliases()` |
| **TestContainers properties not being read** | Ensure `testcontainers.properties` is in `src/test/resources` and properly formatted |
| **Container cleanup not happening** | Check Ryuk container is running: `docker ps | grep ryuk`. Ensure tests complete normally without hanging |
## 사용 가능한 모듈
| 모듈 | Maven 아티팩트 | 설명 |
|--------|---------------|-------------|
| PostgreSQL | `testcontainers-postgresql` | PostgreSQL 데이터베이스 컨테이너 |
| MySQL | `testcontainers-mysql` | MySQL 데이터베이스 컨테이너 |
| MongoDB | `testcontainers-mongodb` | MongoDB 문서 데이터베이스 |
| Kafka | `testcontainers-kafka` | Apache Kafka 메시지 브로커 |
| Redis | `testcontainers-redis` | Redis 인메모리 데이터 스토어 |
| Elasticsearch | `testcontainers-elasticsearch` | Elasticsearch 검색 엔진 |
| Cassandra | `testcontainers-cassandra` | Cassandra NoSQL 데이터베이스 |
| RabbitMQ | `testcontainers-rabbitmq` | RabbitMQ 메시지 브로커 |
| Selenium | `testcontainers-selenium` | 브라우저 자동화 컨테이너 |
| LocalStack | `testcontainers-localstack` | AWS 서비스 에뮬레이션 || Nginx