통합 테스트에 테스트컨테이너 도입

2024. 6. 25. 15:07프로젝트/[EATceed] 몸무게 증량 어플

728x90


프로젝트를 진행하며 개발집의 테스트에 대해 고민하게 되었습니다.

 

현재 프로젝트에서 테스트의 상당수를 차지하고 있는 것은 통합 테스트입니다. 통합 테스트는 애플리케이션의 모든 구성 요소가 개발자의 예상대로 작동하는지 확인하는 것입니다. 그러나 기존 개발팀의 통합 테스트는 서비스 환경의 DB와는 달리 H2를 사용하고 있었습니다.

또한, Redis의 경우에는 테스트 전 수동으로 실행하고 있었습니다.

 

문제점

이러한 방식은 실제 환경과 다른 데이터베이스를 사용하기 때문에 테스트의 신뢰성을 보장하기 어렵고, 테스트 시마다 Redis를 수동으로 실행해야 하므로 개발자의 생산성이 저하되며, 개발자의 로컬 환경마다 테스트 데이터가 상이할 수 있다는 문제가 있었습니다.

 

 

따라서, 통합 테스트 시 DB를 세팅하는 방법에 대해 생각해보았습니다.

 

테스트용 DB를 세팅하는 방법

테스트용 DB를 설정하는 방법에는 여러 가지가 있지만, 그 중 세 가지 방법을 비교해 보았습니다.

  1. Docker Container 사용하여 DB를 세팅
    • Docker를 이용해 MariaDB 컨테이너를 생성하고 이를 테스트에 활용할 수 있습니다.
    • 그러나 테스트를 실행하기 전에 Docker 컨테이너를 실행시켜야 하므로 번거로울 수 있습니다.
    • 또한, 테스트 데이터를 동일하게 유지하기 위해 추가적인 리소스가 필요합니다.
  2. Local에 직접 DB를 설치하여 사용
    • 로컬 환경에 MariaDB를 직접 설치하고 이를 테스트에 사용할 수 있습니다.
    • 하지만, 이 역시 테스트 실행 전마다 DB를 관리하고 초기화해야 하므로 번거로울 수 있습니다.
  3. In-Memory H2를 사용
    • 기존에는 속도 측면에서 유리한 H2를 사용했지만, 이는 실제 서비스 환경과 다른 DB를 사용하므로 통합 테스트의 의미에 맞지 않는다고 판단했습니다.

테스트 컨테이너 선택

위의 방법들은 각각 장단점이 있지만, 테스트의 편의성과 실제 서비스 환경과의 일관성을 유지하기 위해 테스트 컨테이너를 도입하기로 하였습니다.

테스트 컨테이너의 장점

  • 테스트의 편의성: 테스트 실행 시 자동으로 DB 컨테이너가 생성되고 종료되므로, 별도의 DB 관리가 필요 없습니다.
  • 멱등성 유지: 각 테스트가 독립적으로 실행되기 때문에 데이터 간섭 없이 일관된 결과를 얻을 수 있습니다.
  • 실제 서비스와 동일한 환경: H2가 아닌 실제 서비스에서 사용하는 MariaDB를 테스트 환경에서도 동일하게 사용할 수 있습니다.


물론 기존 테스트 방식과 비교하면, 테스트의 속도가 느려졌지만 테스트의 안정성이 더 중요하다고 생각하였습니다.



추가적으로 In-Memory H2 대신에 MariaDB를 사용하는 이유는 DB Schema와 Entity 간의 매핑이 잘 되었는지를 CI 상에서 검사하기 위해서도 있습니다. 이 부분에 대해서는 다음 포스팅에서 자세히 다루도록 하겠습니다.


https://github.com/JNU-econovation/EATceed/pull/322

 

[BE/Feat] 테스트 컨테이너를 사용해 멱등성있는 테스트 환경 구축 by hwangdaesun · Pull Request #322 · JNU

⚠️ 관련 이슈 #214 📢 주요 변경사항 싱글톤을 사용하여 MariaDB와 Redis 컨테이너를 생성하는 ContainerTest 클래스 생성 testData.sql CURRENT_TIMESTAMP이용하여 항상 최신 날짜를 기준으로 하도록 수정 테

github.com

 

핵심 코드

 

@ActiveProfiles("test")
public abstract class ContainerTest {
    static final String MARIA_DB_IMAGE = "mariadb:10.6";
    static final MariaDBContainer Maria_DB_CONTAINER;
    static final String REDIS_IMAGE = "redis:6-alpine";
    static final GenericContainer REDIS_CONTAINER;

    static {
        Maria_DB_CONTAINER = new MariaDBContainer(MARIA_DB_IMAGE);
        Maria_DB_CONTAINER.start();
        REDIS_CONTAINER =
                new GenericContainer<>(REDIS_IMAGE).withExposedPorts(6379).withReuse(true);
        REDIS_CONTAINER.start();
    }

    @DynamicPropertySource
    public static void overrideProps(DynamicPropertyRegistry registry) {
        registry.add("spring.redis.host", REDIS_CONTAINER::getHost);
        registry.add("spring.redis.port", () -> "" + REDIS_CONTAINER.getMappedPort(6379));
    }
}

 

 

static을 사용하여 싱글톤 전략을 통해 모든 테스트가 하나의 MariaDB 컨테이너와 Redis 컨테이너를 공유하도록 구성했습니다. 데이터 격리는 기존에 사용하던 DatabaseCleaner 클래스를 통해 처리하였습니다. 이를 통해 테스트 컨테이너의 단점인 실행 시간을 일부 보완할 수 있었습니다.

 

또한, @DynamicPropertySource를 사용하여 Spring 컨텍스트가 초기화되기 전에 Redis의 호스트와 포트 값을 가져와 컨텍스트 초기화에 사용하였습니다. 이로써 각 테스트 실행 전에 필요한 환경 설정을 동적으로 적용할 수 있었습니다.

 

스프링 컨텍스트 초기화와 자바의 static 메모리 초기화에 대해 간단히 설명드리자면, 자바 클래스가 처음 로드될 때 정적(static) 변수와 초기화 블록이 메서드 영역(Method Area)에 저장되고 초기화됩니다. Spring 컨텍스트는 애플리케이션 실행 시 빈 정의를 로드하고, 의존성 주입 및 초기화를 수행합니다. @DynamicPropertySource 어노테이션을 통해 정적 메서드에서 설정 값을 주입함으로써, Spring 컨텍스트 초기화 전에 필요한 설정을 동적으로 적용할 수 있습니다.



참고)

https://testcontainers.com/guides/testcontainers-container-lifecycle/

 

Testcontainers container lifecycle management using JUnit 5

This guide will explain how to manage container lifecycle with Testcontainers using JUnit 5 lifecycle callbacks and JUnit 5 Extension annotations. We will also look into how to use Singleton Containers pattern to use same containers for multiple tests.

testcontainers.com

 

 

글을 읽어주셔서 감사합니다!

 

혹여나 글을 읽고 조언해주시면 너무나 감사하겠습니다.

728x90