TestContainers: 테스트에서 도커 컨테이너를 실행할 수 있도록 하는 라이브러리

www.testcontainers.org/

 

Testcontainers

 Testcontainers About Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. Testcontainers make the followi

www.testcontainers.org

 

DB의 경우 DB마다 isolation, propagation 전략들이 다르다. (스프링에서는 DB에서 제공하는 이러한 전략들을 따를 뿐이다)

 

하지만 컨테이너의 테스트용 디비를 띄우고, 스크립트를 실행하고, 테스트가 끝난 후에는 컨테이너를 정리하는 등 복잡해질 수 있다.

이를 해결하기 위한 방법이 바로 TestContainers이다.

 

먼저 junit 확장체를 지원하는 testcontainers 모듈을 gradle에 추가한다.

여러 모듈 중에 현재 프로젝트에서 사용하고 있는 DB 모듈을 동일하게 사용하면 된다. 내 경우는 MariaDB를 사용하고 있음.

www.testcontainers.org/modules/databases/mariadb/

 

MariaDB Module - Testcontainers

 

www.testcontainers.org

testImplementation "org.testcontainers:junit-jupiter:1.15.1"
testCompile "org.testcontainers:mariadb:1.15.1"
@ActiveProfiles("test")
@Testcontainers
@SpringBootTest
class ToyRepositoryTest {

    @Autowired
    private ToyRepository toyRepository;

    @Autowired
    private OrganizationRepository organizationRepository;

    static MariaDBContainer mariaDB = new MariaDBContainer().withDatabaseName("toy_land_test");

    @BeforeAll
    static void beforeAll() {
        mariaDB.start();
    }

    @AfterAll
    static void afterAll() {
        mariaDB.stop();
    }
    
    @Test
    /* 테스트 */
}
spring:
  jpa:
    open-in-view: false
    generate-ddl: true
    show-sql: true
    hibernate:
      ddl-auto: create-drop
  datasource:
    url: jdbc:tc:mariadb:///toy_land_test
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver

 

위 코드의 경우 테스트를 실행시키기 전에 컨테이너를 띄우고 실행 후에는 컨테이너를 없애는 것이 반복되어 느릴 수 있다.

아래 코드처럼 컨테이너를 static으로 띄워두고 테스트를 실행 시키기 전에 DB를 비우는 방식으로 코드를 짤 수도 있다. (위의 경우보다 빠를 것이다.)

package com.openhack.toyland.domain.toy;

import static org.assertj.core.api.Assertions.*;
import com.openhack.toyland.domain.Organization;
import com.openhack.toyland.domain.OrganizationRepository;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

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

    @Autowired
    private ToyRepository toyRepository;

    @Autowired
    private OrganizationRepository organizationRepository;

    @Container
    static MariaDBContainer mariaDB = new MariaDBContainer().withDatabaseName("toy_land_test");

    @BeforeEach
    void beforeEach() {
        toyRepository.deleteAll();
        organizationRepository.deleteAll();
    }

    @Test
    /* 테스트 */
}

 

 

TestContainers가 제공하지 않은 일반적인 컨테이너를 만드는 방법 

  • GenericContainer(dockerImageName): 이미지 이름만 있으면 컨테이너를 만드는 것이 가능하다. 이 경우 먼저 로컬에서 이미지를 찾아보고, 로컬에 없으면 퍼블릭한 원격에서 찾아서 가져온다.
    • withEnv: 환경 변수 세팅이 필요하다.
    • withExposedPorts: 포트 노출 -> 일반적으로는 컨테이너가 랜덤하게 매핑해준다
    • waitingFor: 해당 컨테이너가 띄워졌는지 확인 후 테스트 실행

 

TestContainers의 로그 살펴보기

  • getLogs(): 모든 로그를 보는 방법
  • followOutput(new Slf4jLogConsumer(log)): 로그 스트리밍

 

컨테이너 내부의 정보를 테스트 코드에서 활용하는 방법

ApplicationContextInitializer
스프링 ApplicationContext를 프로그래밍으로 초기화 할 때 사용할 수 있는 콜백 인터페이스로, 특정 프로파일을 활성화 하거나, 프로퍼티 소스를 추가하는 등의 작업을 할 수 있다.

TestPropertyValues
테스트용 프로퍼티 소스를 정의할 때 사용한다.

Environment
스프링 핵심 API로, 프로퍼티와 프로파일을 담당한다.

Testcontainer를 사용해서 생성한 컨테이너에 대해 ApplicationContextInitializer를 구현하여 생성된 컨테이너에서 정보를 Environment에 넣어준다.

그 다음 @ContextConfiguration을 사용해서 아까 구현했던 ApplicationContextInitializer 구현체를 등록해주면, 테스트 코드에서 Environment, @Value, @ConfigurationProperties 등 다양한 방법으로 프로퍼티를 사용할 수 있게 된다.

@ActiveProfiles("test")
@Testcontainers
@Slf4j
@ContextConfiguration(initializers = StudyServiceTest.ContainerPropertyInitializer.class)
class StudyServiceTest {

    @Mock MemberService memberService;

    @Autowired StudyRepository studyRepository;

    @Value("${container.port}") int port;

    @Container
    static GenericContainer postgreSQLContainer = new GenericContainer("postgres")
            .withExposedPorts(5432)
            .withEnv("POSTGRES_DB", "studytest");
    @BeforeAll
    static void beforeAll() {
        Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(log);
        postgreSQLContainer.followOutput(logConsumer);
    }
    
    @BeforeEach
    void beforeEach() {
        System.out.println("===========");
        System.out.println(postgreSQLContainer.getMappedPort(5432));
        System.out.println(port);
        studyRepository.deleteAll();
    }

    @Test
    /* 테스트 */

    static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext context) {
            TestPropertyValues.of("container.port=" + postgreSQLContainer.getMappedPort(5432))
                    .applyTo(context.getEnvironment());
        }
    }

}

 

 

 

 

만약 테스트 컨테이너들을 여러개를 동시에 띄워서 사용해야 한다면? -> DockerComposeContainer

www.testcontainers.org/modules/docker_compose/

 

Docker Compose Module - Testcontainers

 Docker Compose Module Benefits Similar to generic containers support, it's also possible to run a bespoke set of services specified in a docker-compose.yml file. This is intended to be useful on projects where Docker Compose is already used in dev or o

www.testcontainers.org

static DockerComposeContainer composeContainer =
    new DockerComposeContainer(new File("src/test/resources/docker-compose-test.yml"))
            .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
            .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

위와 같이 docker-compose 파일을 주면 여러 컨테이너를 띄울 수 있다. (이때, 호스트쪽 포트 매핑은 안해두는 것이 낫다)

아직 컨테이너가 뜨지도 않았는데 테스트를 실행하는 것을 방지하기 위해 Wait.forListeningPort()를 추가 해 주는 것이 좋다.

 

위의 경우 docker-compose로 띄운 서비스의 정보를 참조하기 위해 구현하는 ApplicationContextInitializer는 아래와 같이 구현하면 된다.

static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        TestPropertyValues.of("container.port=" + composeContainer.getServicePort("study-db", 5432))
                .applyTo(context.getEnvironment());
    }
}

 

 

+ Recent posts