spring / / 2025. 10. 10. 06:33

[Spring Boot 번역] Testcontainers

출처: https://docs.spring.io/spring-boot/4.0-SNAPSHOT/reference/testing/testcontainers.html


중요: 이 버전은 아직 개발 중이며 안정적인 것으로 간주되지 않습니다. 최신 안정 버전은 Spring Boot 3.5.6을 사용하세요!

Testcontainers

Testcontainers 라이브러리는 Docker 컨테이너 내부에서 실행되는 서비스를 관리하는 방법을 제공합니다. JUnit과 통합되어 테스트 클래스가 테스트 실행 전에 컨테이너를 시작할 수 있게 해줍니다. Testcontainers는 MySQL, MongoDB, Cassandra 등과 같은 실제 백엔드 서비스와 통신하는 통합 테스트를 작성하는 데 특히 유용합니다.

다음 섹션에서는 Testcontainers를 테스트와 통합하는 데 사용할 수 있는 몇 가지 방법을 설명합니다.

Using Spring Beans

Testcontainers가 제공하는 컨테이너는 Spring Boot에서 빈(Bean)으로 관리될 수 있습니다.

컨테이너를 빈으로 선언하려면 테스트 구성에 @Bean 메서드를 추가하세요:

Java:

import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;


@TestConfiguration(proxyBeanMethods = false)
class MyTestConfiguration {

    @Bean
    MongoDBContainer mongoDbContainer() {
        return new MongoDBContainer(DockerImageName.parse("mongo:5.0"));
    }

}

Kotlin:

import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName

import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean


@TestConfiguration(proxyBeanMethods = false)
class MyTestConfiguration {

    @Bean
    fun mongoDbContainer(): MongoDBContainer {
        return MongoDBContainer(DockerImageName.parse("mongo:5.0"))
    }

}

그런 다음 테스트 클래스에서 구성 클래스를 가져와서 컨테이너를 주입하고 사용할 수 있습니다:

Java:

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MongoDBContainer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;


@SpringBootTest
@Import(MyTestConfiguration.class)
class MyIntegrationTests {

    @Autowired
    private MongoDBContainer mongo;

    @Test
    void myTest() {
        ...
    }

}

Kotlin:

import org.junit.jupiter.api.Test
import org.testcontainers.containers.MongoDBContainer

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import


@SpringBootTest
@Import(MyTestConfiguration::class)
class MyIntegrationTests {

    @Autowired
    private lateinit var mongo: MongoDBContainer

    @Test
    fun myTest() {
        ...
    }

}

이 컨테이너 관리 방법은 종종 서비스 연결 애노테이션과 함께 사용됩니다.

Using the JUnit Extension

Testcontainers는 테스트에서 컨테이너를 관리하는 데 사용할 수 있는 JUnit 확장을 제공합니다. 이 확장은 Testcontainers의 @Testcontainers 애노테이션을 테스트 클래스에 적용하여 활성화됩니다.

그런 다음 정적(static) 컨테이너 필드에 @Container 애노테이션을 사용할 수 있습니다.

@Testcontainers 애노테이션은 일반적인 JUnit 테스트에서 사용하거나 @SpringBootTest와 함께 사용할 수 있습니다:

Java:

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;


@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

    @Test
    void myTest() {
        ...
    }

}

Kotlin:

import org.junit.jupiter.api.Test
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

import org.springframework.boot.test.context.SpringBootTest


@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    val neo4j = Neo4jContainer<Nothing>("neo4j:5")

    @Test
    fun myTest() {
        ...
    }

}

위의 예제는 테스트가 실행되기 전에 Neo4j 컨테이너를 시작합니다. 컨테이너 인스턴스의 생명주기는 공식 문서에 설명된 대로 Testcontainers에서 관리합니다.

대부분의 경우 애플리케이션이 컨테이너에서 실행 중인 서비스에 연결하도록 추가로 구성해야 합니다.

Importing Container Configuration Interfaces

Testcontainers의 일반적인 패턴은 인터페이스의 정적(static) 필드로 컨테이너 인스턴스를 선언하는 것입니다.

예를 들어, 다음 인터페이스는 MongoDBContainer 타입의 mongoNeo4jContainer 타입의 neo4j라는 두 개의 컨테이너를 선언합니다:

Java:

import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;


interface MyContainers {

    @Container
    MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");

    @Container
    Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");

}

Kotlin:

import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container


interface MyContainers {

    companion object {
        @Container
        val mongoContainer = MongoDBContainer("mongo:5.0")

        @Container
        val neo4jContainer = Neo4jContainer<Nothing>("neo4j:5")
    }

}

이렇게 컨테이너를 선언하면 테스트 클래스가 인터페이스를 구현하여 여러 테스트에서 해당 구성을 재사용할 수 있습니다.

Spring Boot 테스트에서도 동일한 인터페이스 구성을 사용할 수 있습니다. 이렇게 하려면 테스트 구성 클래스에 @ImportTestcontainers를 추가하세요:

Java:

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;


@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers.class)
class MyTestConfiguration {

}

Kotlin:

import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.context.ImportTestcontainers


@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyTestConfiguration

Lifecycle of Managed Containers

Testcontainers에서 제공하는 애노테이션과 확장을 사용한 경우, 컨테이너 인스턴스의 생명주기는 전적으로 Testcontainers에서 관리합니다. 정보는 공식 Testcontainers 문서를 참조하세요.

컨테이너가 Spring에서 빈으로 관리되는 경우 생명주기는 Spring에서 관리합니다:

  • 컨테이너 빈은 다른 모든 빈보다 먼저 생성되고 시작됩니다.
  • 컨테이너 빈은 다른 모든 빈이 소멸된 후에 중지됩니다.

이 프로세스는 컨테이너가 제공하는 기능에 의존하는 모든 빈이 해당 기능을 사용할 수 있도록 보장합니다. 또한 컨테이너가 여전히 사용 가능한 동안 정리되도록 보장합니다.

애플리케이션 빈이 컨테이너의 기능에 의존하는 경우 올바른 생명주기 동작을 보장하기 위해 컨테이너를 Spring 빈으로 구성하는 것이 좋습니다.

Testcontainers에서 컨테이너를 관리하는 것은 Spring 빈으로 관리하는 것과 달리 빈과 컨테이너가 종료되는 순서를 보장하지 않습니다. 컨테이너 기능에 의존하는 빈이 정리되기 전에 컨테이너가 종료될 수 있습니다. 이로 인해 예를 들어 연결 손실로 인해 클라이언트 빈에서 예외가 발생할 수 있습니다.

컨테이너 빈은 Spring의 TestContext Framework에서 관리하는 애플리케이션 컨텍스트당 한 번 생성되고 시작됩니다. TestContext Framework가 기본 애플리케이션 컨텍스트와 그 안의 빈을 관리하는 방법에 대한 자세한 내용은 Spring Framework 문서를 참조하세요.

컨테이너 빈은 TestContext Framework의 표준 애플리케이션 컨텍스트 종료 프로세스의 일부로 중지됩니다. 애플리케이션 컨텍스트가 종료되면 컨테이너도 종료됩니다. 이는 일반적으로 특정 캐시된 애플리케이션 컨텍스트를 사용하는 모든 테스트가 실행을 완료한 후에 발생합니다. TestContext Framework에 구성된 캐싱 동작에 따라 더 일찍 발생할 수도 있습니다.

단일 테스트 컨테이너 인스턴스는 여러 테스트 클래스의 테스트 실행에서 유지될 수 있으며 종종 그렇습니다.

Service Connections

서비스 연결은 모든 원격 서비스에 대한 연결입니다. Spring Boot의 자동 구성은 서비스 연결의 세부 정보를 사용하여 원격 서비스에 대한 연결을 설정할 수 있습니다. 이렇게 하면 연결 세부 정보가 연결 관련 구성 속성보다 우선합니다.

Testcontainers를 사용할 때 테스트 클래스의 컨테이너 필드에 애노테이션을 추가하여 컨테이너에서 실행 중인 서비스에 대한 연결 세부 정보를 자동으로 생성할 수 있습니다.

Java:

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;


@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    @ServiceConnection
    static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

    @Test
    void myTest() {
        ...
    }

}

Kotlin:

import org.junit.jupiter.api.Test
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection


@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    @ServiceConnection
    val neo4j = Neo4jContainer<Nothing>("neo4j:5")

    @Test
    fun myTest() {
        ...
    }

}

@ServiceConnection 덕분에 위의 구성을 통해 애플리케이션의 Neo4j 관련 빈이 Testcontainers가 관리하는 Docker 컨테이너 내에서 실행 중인 Neo4j와 통신할 수 있습니다. 이는 Neo4jConnectionDetails 빈을 자동으로 정의하여 Neo4j 자동 구성에서 사용되며 연결 관련 구성 속성을 재정의합니다.

Testcontainers와 함께 서비스 연결을 사용하려면 spring-boot-testcontainers 모듈을 테스트 종속성으로 추가해야 합니다.

서비스 연결 애노테이션은 spring.factories에 등록된 ContainerConnectionDetailsFactory 클래스에서 처리됩니다. ContainerConnectionDetailsFactory는 특정 Container 하위 클래스 또는 Docker 이미지 이름을 기반으로 ConnectionDetails 빈을 생성할 수 있습니다.

다음 서비스 연결 팩토리가 spring-boot-testcontainers jar에 제공됩니다:

Connection Details Matched on
ActiveMQConnectionDetails "symptoma/activemq"라는 이름의 컨테이너 또는 ActiveMQContainer
ArtemisConnectionDetails ArtemisContainer 타입의 컨테이너
CassandraConnectionDetails CassandraContainer 타입의 컨테이너
CouchbaseConnectionDetails CouchbaseContainer 타입의 컨테이너
ElasticsearchConnectionDetails ElasticsearchContainer 타입의 컨테이너
FlywayConnectionDetails JdbcDatabaseContainer 타입의 컨테이너
JdbcConnectionDetails JdbcDatabaseContainer 타입의 컨테이너
KafkaConnectionDetails KafkaContainer, ConfluentKafkaContainer 또는 RedpandaContainer 타입의 컨테이너
LdapConnectionDetails "osixia/openldap"라는 이름의 컨테이너 또는 LLdapContainer 타입의 컨테이너
LiquibaseConnectionDetails JdbcDatabaseContainer 타입의 컨테이너
MongoConnectionDetails MongoDBContainer 또는 MongoDBAtlasLocalContainer 타입의 컨테이너
Neo4jConnectionDetails Neo4jContainer 타입의 컨테이너
OpenTelemetryLoggingConnectionDetails "otel/opentelemetry-collector-contrib"라는 이름의 컨테이너 또는 LgtmStackContainer 타입의 컨테이너
OtlpMetricsConnectionDetails "otel/opentelemetry-collector-contrib"라는 이름의 컨테이너 또는 LgtmStackContainer 타입의 컨테이너
OtlpTracingConnectionDetails "otel/opentelemetry-collector-contrib"라는 이름의 컨테이너 또는 LgtmStackContainer 타입의 컨테이너
PulsarConnectionDetails PulsarContainer 타입의 컨테이너
R2dbcConnectionDetails ClickHouseContainer, MariaDBContainer, MSSQLServerContainer, MySQLContainer, OracleContainer (free), OracleContainer (XE) 또는 PostgreSQLContainer 타입의 컨테이너
RabbitConnectionDetails RabbitMQContainer 타입의 컨테이너
RedisConnectionDetails RedisContainer 또는 RedisStackContainer 타입의 컨테이너, 또는 "redis", "redis/redis-stack" 또는 "redis/redis-stack-server"라는 이름의 컨테이너
ZipkinConnectionDetails "openzipkin/zipkin"라는 이름의 컨테이너

기본적으로 주어진 Container에 대해 적용 가능한 모든 연결 세부 정보 빈이 생성됩니다. 예를 들어, PostgreSQLContainerJdbcConnectionDetailsR2dbcConnectionDetails를 모두 생성합니다.

적용 가능한 타입의 하위 집합만 생성하려면 @ServiceConnectiontype 속성을 사용할 수 있습니다.

기본적으로 Container.getDockerImageName().getRepository()를 사용하여 연결 세부 정보를 찾는 데 사용되는 이름을 가져옵니다. Docker 이미지 이름의 리포지토리 부분은 레지스트리와 버전을 무시합니다. 이는 위의 예제와 같이 정적 필드를 사용할 때 Spring Boot가 Container 인스턴스를 가져올 수 있는 한 작동합니다.

@Bean 메서드를 사용하는 경우 Spring Boot는 빈 메서드를 호출하여 Docker 이미지 이름을 가져오지 않습니다. 이는 조기 초기화 문제를 일으킬 수 있기 때문입니다. 대신 빈 메서드의 반환 타입을 사용하여 어떤 연결 세부 정보를 사용해야 하는지 확인합니다. 이는 Neo4jContainer 또는 RabbitMQContainer와 같은 타입이 지정된 컨테이너를 사용하는 한 작동합니다. 다음 예제와 같이 Redis와 함께 GenericContainer를 사용하는 경우 작동하지 않습니다:

Java:

import org.testcontainers.containers.GenericContainer;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;


@TestConfiguration(proxyBeanMethods = false)
public class MyRedisConfiguration {

    @Bean
    @ServiceConnection(name = "redis")
    public GenericContainer<?> redisContainer() {
        return new GenericContainer<>("redis:7");
    }

}

Kotlin:

import org.testcontainers.containers.GenericContainer

import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Bean


@TestConfiguration(proxyBeanMethods = false)
class MyRedisConfiguration {

    @Bean
    @ServiceConnection(name = "redis")
    fun redisContainer(): GenericContainer<*> {
        return GenericContainer("redis:7")
    }

}

Spring Boot는 GenericContainer에서 어떤 컨테이너 이미지가 사용되는지 알 수 없으므로 @ServiceConnectionname 속성을 사용하여 해당 힌트를 제공해야 합니다.

또한 @ServiceConnectionname 속성을 사용하여 사용자 정의 이미지를 사용할 때 어떤 연결 세부 정보가 사용될지 재정의할 수 있습니다. Docker 이미지 registry.mycompany.com/mirror/myredis를 사용하는 경우 @ServiceConnection(name="redis")를 사용하여 RedisConnectionDetails가 생성되도록 할 수 있습니다.

SSL with Service Connections

지원되는 컨테이너에서 @Ssl, @JksKeyStore, @JksTrustStore, @PemKeyStore@PemTrustStore 애노테이션을 사용하여 해당 서비스 연결에 대한 SSL 지원을 활성화할 수 있습니다. 애노테이션은 애플리케이션의 클라이언트 측에서만 SSL을 구성하므로 Testcontainer 내부에서 실행 중인 서비스에서 SSL을 직접 활성화해야 합니다.

import com.redis.testcontainers.RedisContainer;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.PemKeyStore;
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.data.redis.core.RedisOperations;


@Testcontainers
@SpringBootTest
class MyRedisWithSslIntegrationTests {

    @Container
    @ServiceConnection
    @PemKeyStore(certificate = "classpath:client.crt", privateKey = "classpath:client.key")
    @PemTrustStore("classpath:ca.crt")
    static RedisContainer redis = new SecureRedisContainer("redis:latest");

    @Autowired
    private RedisOperations<Object, Object> operations;

    @Test
    void testRedis() {
        // ...
    }

}

위의 코드는 @PemKeyStore 애노테이션을 사용하여 클라이언트 인증서와 키를 keystore에 로드하고 @PemTrustStore 애노테이션을 사용하여 CA 인증서를 truststore에 로드합니다. 이것은 서버에 대해 클라이언트를 인증하고, truststore의 CA 인증서는 서버 인증서가 유효하고 신뢰할 수 있는지 확인합니다.

이 예제의 SecureRedisContainer는 인증서를 올바른 위치에 복사하고 SSL을 활성화하는 명령줄 매개변수로 redis-server를 호출하는 RedisContainer의 사용자 정의 하위 클래스입니다.

SSL 애노테이션은 다음 서비스 연결에 지원됩니다:

  • Cassandra
  • Couchbase
  • Elasticsearch
  • Kafka
  • MongoDB
  • RabbitMQ
  • Redis

ElasticsearchContainer는 서버 측 SSL의 자동 감지를 추가로 지원합니다. 이 기능을 사용하려면 다음 예제와 같이 컨테이너에 @Ssl로 애노테이션을 추가하면 Spring Boot가 클라이언트 측 SSL 구성을 자동으로 처리합니다:

import org.junit.jupiter.api.Test;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.elasticsearch.test.autoconfigure.DataElasticsearchTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testcontainers.service.connection.Ssl;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;


@Testcontainers
@DataElasticsearchTest
class MyElasticsearchWithSslIntegrationTests {

    @Ssl
    @Container
    @ServiceConnection
    static ElasticsearchContainer elasticsearch = new ElasticsearchContainer(
            "docker.elastic.co/elasticsearch/elasticsearch:8.17.2");

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Test
    void testElasticsearch() {
        // ...
    }

}

Dynamic Properties

서비스 연결에 대한 약간 더 장황하지만 더 유연한 대안은 @DynamicPropertySource입니다. 정적(static) @DynamicPropertySource 메서드를 사용하면 Spring Environment에 동적 속성 값을 추가할 수 있습니다.

Java:

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;


@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

    @Test
    void myTest() {
        // ...
    }

    @DynamicPropertySource
    static void neo4jProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
    }

}

Kotlin:

import org.junit.jupiter.api.Test
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource


@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    val neo4j = Neo4jContainer<Nothing>("neo4j:5")

    @Test
    fun myTest() {
        // ...
    }

    companion object {
        @DynamicPropertySource
        @JvmStatic
        fun neo4jProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.neo4j.uri", neo4j::getBoltUrl)
        }
    }

}

위의 구성을 사용하면 애플리케이션의 Neo4j 관련 빈이 Testcontainers가 관리하는 Docker 컨테이너 내부에서 실행 중인 Neo4j와 통신할 수 있습니다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유