본문 바로가기
개발

TestContainers 소개

by 상5c 2022. 7. 24.

TestContainers

TestContainers는 JUnit 테스트를 지원하는 Java 라이브러리이다. 테스트 시작시 도커 컨테이너를 띄워주고 테스트 종료시 컨테이너를 제거해준다.
데이터베이스를 포함한 통합 테스트를 위해 사용되며, 실제 데이터베이스 컨테이너를 띄우기 때문에 H2 데이터베이스를 사용한 테스트보다 좀 더 정확성 높은 테스트를 제공한다.
단점은 테스트시마다 도커 이미지 빌드, 컨테이너 생성, 실행, 종료, 제거가 수행되기 때문에 테스트 시간이 오래걸린다. 주의사항으로는 테스트시에 도커 이미지만큼의 메모리가 추가로 필요하다.

혼돈을 피하기 위해 역할 정의만 다시 하자면 Java 테스트코드에서 Docker Container 라이프사이클 관리라고 표현할 수 있다. 따라서 테스트, Docker에 대한 이해가 필요하다.
같이 나오는 내용의 대부분은 도커 또는 스프링 부트의 설정이다. 글의 목적은 TestContainers이기 때문에 이외의 부분에 대해서는 자세한 설명은 생략했다.

사용해보기

의존성 추가

// build.gradle
ext {
    set('testcontainersVersion', "1.16.2")
}

dependencies {
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
}

dependencyManagement {
    imports {
        mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
    }
}
  • Spring boot initializer를 사용하면 위와 같은 의존성을 추가해준다.
  • PostgreSQLContainer를 직접 사용하지 않는다면 postgresql, mssql같은 의존성은 따로 추가해주지 않아도 된다.
    • docker compose yml 파일을 통해 테스트 컨테이너를 구성하는 경우, 또는 GenericContainer를 사용하면 testImplementation 'org.testcontainers:junit-jupiter'만 추가하면 된다.

기본 사용법

@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class SampleRepositoryTest {

    @Container
    static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres").withDatabaseName("test");

    @Test
    void test() {
        System.out.println("hello world!")
    }
}

PostgreSQLContainer 객체를 사용하더라도 이미지명을 명시하는 것을 권장한다. 생성자를 통해 이미지명을 전달할 수 있다.

@TestContainers 어노테이션과 @Container 어노테이션이 docker start/stop 처리를 지원한다.

  • 수동으로 처리하고 싶다면 @BeforeAll, @AfterAll 어노테이션이 붙은 메서드에서 postgresSQLContainer.start(), stop()을 호출해줘야 한다.
  • 수동 처리하는 경우 @TestContainers, @Container 어노테이션을 사용하지 않아도 된다.

static으로 사용시 모든 테스트에서 동일한 docker 컨테이너를 사용하며, static 키워드를 제거하면 각 테스트마다 docker 컨테이너가 새로 만들어진다.

위 테스트를 실행한 후 docker stats 명령어를 통해 컨테이너가 실행되는 것을 볼 수 있다.

테스트시 Ryuk container가 같이 실행되는데, 이는 테스트컨테이너를 관리(ambassador)하는 역할을 한다. ryuk container를 사용하고 싶지 않으면 TESTCONTAINERS_RYUK_DISABLED=true 환경 변수를 세팅해주면 되며, 테스트 종료 후 사용되었던 도커 컨테이너가 삭제되지 않는 것을 확인할 수 있다. 비활성화가 꼭 필요하다면 링크를 통해 설정을 참고하자. 메모리 사용량도 낮아서 굳이 비활성화 할 이유를 찾지 못했다.

또한 datasource를 아래와 같은 식으로 전달할 수 있는데, ryuk container를 통해 연결되는 것으로 보인다.

spring:
  datasource:
    url: jdbc:tc:postgresql:///test
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver

환경 변수 전달

컨테이너 생성시 여러 환경 변수를 전달할 수 있다.

static GenericContainer<?> postgresSQLContainer2 = new GenericContainer<>("postgres")
            .withExposedPorts(15432) // 주의! 외부 포트가 아님!
            .withEnv("POSTGRES_USER", "test")
            .withEnv("POSTGRES_PASSWORD", "pwd")
            .withEnv("POSTGRES_DB", "test")
            .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust");

여기서 주의사항은 withExposedPorts()는 컨테이너 입장에서의 포트 설정이다. 외부 포트는 랜덤하게 지정되며, 이 랜덤 포트가 내부의 어떤 포트로 연결될지에 대한 설정이다.

docker ps 명령어를 실행한 결과

  • 외부 포트가 63180으로 매핑된 것을 확인할 수 있다.

참고: https://www.testcontainers.org/features/networking/

따라서 실제로 어떤 포트로 연결해야할지 알고싶다면 추가적인 코드가 필요하다.이에 대한 자세한 설명은 Baeldung을 통해 확인할 수 있으며, @DynamicPropertySource를 사용하는 코드가 가장 최신 버전이다.

아래 코드는 실제 매핑된 포트를 읽어, 동적으로 properties 설정을 진행한다.

@DynamicPropertySource
static void registerPgProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", 
      () -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()));
    registry.add("spring.datasource.username", () -> "postgres");
    registry.add("spring.datasource.password", () -> "pass");
}

참고: Spring Boot Docs

로그 스트리밍

컨테이너 내에서 찍히는 로그에 대해 스트리밍이 가능하다.

Container 전역 변수에 직접 Consumer를 추가해주거나, BeforeAll에서 추가해준다.

private static final Logger log = LoggerFactory.getLogger(MemberRepositoryTest2.class);

// 방법 1
@Container
static GenericContainer<?> postgresSQLContainer = new GenericContainer<>("postgres")
            .withLogConsumer(new Slf4jLogConsumer(log));

// 방법 2
@BeforeAll
static void beforeAll() {
    Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(log);
    postgresSQLContainer.followOutput(logConsumer);
}

테스트시 Docker compose 사용하기

사용법은 동일하다

public static DockerComposeContainer environment =
    new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
            .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
            .waitingFor("db_1", Wait.forLogMessage("started", 1))
            .withLocalCompose(true);

compose yml 파일을 전달하면 TestContainers에 의해 실행된다.

compose는 외부 포트를 지정할 수 있다.

참고: TestContainers/docker_compose