Test Containers

April 10, 2022

Test Containers

테스트 코드는 실행 환경과 상관 없이 성공, 실패에 대한 멱등성을 유지하는 것이 중요하다. 특히 외부 요인에 대한 의존을 제거할 수 없는 통합 테스트의 경우. 예를 들어 데이터베이스에 대한 테스트가 포함되는 경우 매 테스트 실행 시점마다 데이터베이스 상태를 일관성 있게 유지하지 못한다면 멱등성이 깨질 확률이 높다.

따라서 테스트에 사용하는 데이터베이스는 각 테스트 사이, 다른 어떤 외부 요인에 의해 영향을 받지 않는 고립된 상태여야 한다. 처음에는 h2를 사용한 메모리 DB를 사용하려 했으나, 이는 실제 사용하는 데이터베이스와 완벽하게 일치할 수 없다는 점이 단점이다. 각 데이터베이스의 dialect 부터 고립 레벨 등의 설정의 불일치가 존재하며, 테스트와 운영 환경에 차이가 발생할 수 밖에 없다. 최악의 케이스는 테스트에서는 성공했으나 운영에서는 실패하는 false negative 상황이 발생할 수도 있다는 것이다.

그래서 이왕이면 테스트에서도 실제 환경에서 사용하는 데이터베이스 제품을 그대로 사용하는 것이 좋다. Testcontainers를 사용하면 이를 좀 더 쉽게 구성할 수 있는데, 테스트 시작 시 필요한 이미지들을 실행 시키고 테스트가 종료되면 이를 알아서 정리해주는 역할을 한다. 테스트에 MariaDB가 필요하다고 가정하며, 아래 의존성을 추가한다.

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

JDBC support

JDBC support를 사용하면 별도 설정 없이 각 테스트마다 일회성 DB를 얻을 수 있다. 일반적인 jdbc url은 jdbc:mysql://localhost:3306/databasename의 형태를 지니는데 여기서 jdbc: 뒤에 tc:를 삽입을 하면, 호스트 명, 포트 번호, 데이터베이스 명은 모두 무시되며 Testcontainers에 의해 적절하게 바인딩 된다.

spring:
  datasource:
    url: jdbc:tc:mariadb:10.3:///

JUnit5 Integration

Junit5와의 통합은 @Testcontainers 어노테이션으로 이루어진다. 해당 익스텐션이 붙은 클래스에서는 @Container가 선언된 객체들의 lifecycle method를 호출한다.

@SpringBootTest
@Testcontainers
class UserRepositoryTest {
    @Autowired
    UserRepository userRepository;

    @Container
    MariaDBContainer mariaDB = new MariaDBContainer(DockerImageName.parse("mariadb:10.5"));

    // @Container
    // static final MariaDBContainer mariaDB = new MariaDBContainer(DockerImageName.parse("mariadb:10.5"));

    @Test
    @DisplayName("사용자 조회 테스트")
    void findAllTest() {
        ...
    }
}

위 케이스에서는 각 메서드마다 컨테이너가 시작, 종료된다. 클래스 내에서 컨테이너를 한 번만 시작하고 각 메서드들 사이에서 공유하게하려면 static으로 선언하면 된다. 같은 개념으로 여러 클래스들 사이에서 한 번만 시작되는 컨테이너가 필요할 수도 있다. 하지만 자체적으로 지원하는 기능은 없으며 빈으로 생성하거나, 싱글톤 패턴으로 아래와 같이 구현할 수도 있다.

abstract class AbstractContainerBaseTest {

    static final MySQLContainer MY_SQL_CONTAINER;

    static {
        MY_SQL_CONTAINER = new MySQLContainer();
        MY_SQL_CONTAINER.start();
    }
}

class FirstTest extends AbstractContainerBaseTest {

    @Test
    void someTestMethod() {
        String url = MY_SQL_CONTAINER.getJdbcUrl();

        // create a connection and run test as normal
    }
}

참고


songmk 🙁