본문 바로가기
카테고리 없음

TestContainers 테스트 환경 구축

by sujupark54 2026. 2. 11.

코딩 테스트

테스트를 작성하다 보면 단순한 단위 테스트를 넘어 실제 서버나 데이터베이스와 유사한 환경에서 테스트를 수행해야 하는 경우가 자주 발생한다. 예를 들어 데이터베이스 연동 테스트의 경우 로컬 DB를 직접 연결하거나 H2 인메모리 데이터베이스, 혹은 docker-compose를 활용하는 방법을 고려하게 된다. 각각의 방식은 장단점이 분명하며 테스트 환경과 운영 환경 간의 차이가 문제로 이어지는 경우도 적지 않다.

이러한 문제를 해결하기 위한 대안으로 TestContainers는 테스트 실행 시점에 실제 컨테이너 기반의 인프라를 자동으로 구성하고, 테스트 종료와 함께 정리해 주는 방식으로 테스트 신뢰성과 재현성을 동시에 확보할 수 있도록 돕는다.


TestContainers 개요

TestContainers는 Docker 컨테이너를 자바 코드로 제어할 수 있도록 제공하는 라이브러리다. 테스트 코드 내부에서 컨테이너를 정의하고 실행하며, 테스트 생명주기에 맞춰 자동으로 시작과 종료를 관리한다. 이를 통해 테스트 환경과 실제 운영 환경 간의 차이를 최소화할 수 있다는 점이 가장 큰 장점이다.

특히 데이터베이스, 메시지 브로커, 캐시 서버 등 외부 인프라 의존성이 있는 테스트에서 TestContainers는 높은 효과를 발휘한다. 로컬 환경에 별도의 서버를 띄울 필요 없이 테스트 코드 실행만으로 동일한 환경을 재현할 수 있기 때문이다.

Gradle 환경에서의 기본 의존성 설정은 다음과 같다.


testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.testcontainers:testcontainers'

TestContainers는 JUnit5와 자연스럽게 통합되며, 컨테이너 생명주기를 테스트 실행 흐름에 맞게 관리해 준다. 테스트가 실패하더라도 컨테이너가 자동으로 정리되기 때문에 테스트 환경 관리에 대한 부담을 크게 줄일 수 있다.


TestContainers 초기화 전략

공식 문서에서는 @DynamicPropertySource를 활용해 컨테이너에서 생성된 설정 값을 스프링 환경에 주입하는 방법을 소개한다. 이 방식은 간단하지만 정적 메서드 제약과 상속 구조를 요구하기 때문에 여러 컨테이너를 조합하는 테스트에서는 구조가 복잡해질 수 있다.

대안으로 ApplicationContextInitializer를 활용하면 컨텍스트가 로드되는 시점에 필요한 환경 설정을 보다 유연하게 주입할 수 있다. 이 방식은 특정 테스트 컨텍스트에 필요한 컨테이너만 선택적으로 등록할 수 있어 테스트 구성의 가독성과 재사용성을 높여준다.

아래는 PostgreSQL 컨테이너를 초기화하는 예시다.


@Testcontainers
public class PostgresContainerInitializer
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private static final String IMAGE = "postgres:latest";

    @Container
    public static final PostgreSQLContainer<?> container =
        new PostgreSQLContainer<>(IMAGE)
            .withDatabaseName("lkdcode")
            .withUsername("lkdcode")
            .withPassword("lkdcode");

    static {
        container.start();
    }

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        Map<String, Object> props = new HashMap<>();
        props.put("spring.datasource.url", container.getJdbcUrl());
        props.put("spring.datasource.username", container.getUsername());
        props.put("spring.datasource.password", container.getPassword());
        context.getEnvironment()
            .getPropertySources()
            .addFirst(new MapPropertySource("postgres", props));
    }
}

이 방식은 컨테이너 설정과 스프링 환경 설정을 명확히 분리할 수 있으며, 테스트마다 필요한 인프라 구성을 조합 형태로 관리할 수 있다는 장점이 있다.


TestContainers 실전 활용

Redis와 같은 캐시 서버 역시 동일한 방식으로 컨테이너화할 수 있다. 이미지 등록과 포트 매핑, 환경 변수 설정 정도만 다를 뿐 구조는 PostgreSQL 컨테이너와 크게 다르지 않다.


@Testcontainers
public class RedisContainerInitializer
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Container
    public static final GenericContainer<?> redis =
        new GenericContainer<>("redis:latest")
            .withExposedPorts(6379);

    static {
        redis.start();
    }

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        Map<String, Object> props = new HashMap<>();
        props.put("spring.data.redis.host", redis.getHost());
        props.put("spring.data.redis.port", redis.getMappedPort(6379));
        context.getEnvironment()
            .getPropertySources()
            .addFirst(new MapPropertySource("redis", props));
    }
}

이렇게 정의한 Initializer들은 @ContextConfiguration을 통해 테스트 클래스에 선택적으로 등록할 수 있다. 테스트마다 필요한 컨테이너 조합을 명시적으로 드러낼 수 있어 테스트 의도를 명확히 표현할 수 있다.

또한 Flyway와 같은 DB 마이그레이션 도구를 함께 사용하면 컨테이너가 매번 새로 생성되기 때문에 히스토리 충돌 없이 스키마 검증과 테스트를 동시에 수행할 수 있다. 이는 테스트 안정성과 신뢰도를 높이는 데 큰 도움이 된다.