ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot 환경에서 컨테이너 테스트 환경 구축하기 w/ Testcontainers
    웹/Spring 2024. 7. 18. 15:10

    테스트 환경 구축의 필요성

    Spring Boot로 개발하다 보면, 내가 작성한 컴포넌트가 올바르게 동작하는지 테스트 할 필요가 있고, 아예 테스트 기반으로 개발을 해나가는 TDD라는 개념도 존재한다.

    아직 테스트 코드를 작성하는데 미숙하지만, 테스트 코드가 주는 이점이 상당하다고 생각한다. 그 중 대표적인 것이 개발 생산성 증가인 것 같다. 테스트 코드를 작성하지 않으면 Postman과 같은 API 테스터기로 테스트를 진행해야 하는데, 이것이 여간 생산성을 좀먹는 존재가 아니다. 더군다나 자동화되지 않은 테스트 방식은 휴먼 에러를 불러일으키기도 매우 쉽다.

     

    다만, API를 테스트 할 때 고려해야 할 것이 바로 외부 모듈(대표적으로 DB)이다. 외부 모듈이 항상 같은 반환값을 내주어야 테스트 결과도 일정해지기 때문에 테스트 코드도 신뢰할 수 있게 되고, Problem isolation도 가능해지기 때문이다. 외부 모듈이 매번 다른 값을 반환한다면 문제가 내 코드에 있는지 외부 모듈에 존재하는지 알 길이 없기 때문이다.

     

    그러한 외부 모듈에는 대표적으로 DB가 있으며, 여지껏 개발을 수행할 때 DB를 인메모리로 임베딩해서 올리거나, 로컬에 설치해서 연동해 개발을 진행했다. 하지만, 이렇게 하니 다음과 같은 한계점이 있다.

     

    인 메모리로 임베딩 하는 경우

    실제 배포 환경에 사용할 DBMS가 지원되지 않는 경우가 왕왕 있다. 가령 MySQL이 인 메모리 DB가 지원되지 않는다. 물론, H2와 같은 데이터베이스로 개발할 수 있으나 배포 DBMS와는 또 다른 SQL을 작성해야 한다는 단점이 있다.

     

    로컬에 설치해서 연동해 개발을 진행하는 경우

    여러 프로젝트를 하나의 로컬에서 개발할 때 DBMS가 겹칠 수 있고, 무엇보다 로컬 머신의 리소스가 상당히 많이 들게 된다. 게다가 테스트가 마무리 된 뒤 남아있는 테스트 데이터도 삭제해주지 않으면 멱등성이 보장되지 않는다. 

     

    궁극적으로, 문서화가 되지 않는다는 점이 가장 큰 문제이다. 내가 DBMS를 로컬 환경에 구축한다고 하더라도, 같이 개발하는 다른이가 나와 동일한 환경을 구축하는 것은 매우 어렵기 때문이다.

     

    이런 저런 이유로 "테스트 환경을 컨테이너로 구축할 수 없나?" 라는 물음에 봉착했고, 이것을 해결하기 위해 이틀 동안 씨름한 것 같다. 처음에는 docker와 docker-compose로 구축해보려다가, gradle과 연동하는 부분에서 막혀 여러 자료를 찾아보았다. 열심히 삽질한 끝에 발견한 라이브러리가 Testcontainers 이다.

     

    Testcontainers 도입

     

    Testcontainers는 Junit 테스트 시작 시 DBMS가 포함된 컨테이너를 실행시키고, 테스트가 모두 완료되면 해당 컨테이너를 종료해주는 아주 유용한 라이브러리이다. Java 환경 뿐 아니라 go, .net, node.js 등 다양한 언어가 지원되니 공식 문서를 확인 바란다.

     

    아직 내공이 많이 부족해 내가 구축한 테스트 환경이 절대 Best Practice가 아니다!! 하지만 일단 구동되는 환경을 구축했으니 이걸 활용해 멱등성이 보장된 테스트를 앞으로 진행해보고자 한다.

     

    간단한 비즈니스 로직 구현

     

    우선, 간단한 비즈니스 로직을 구현해보겠다. 내가 프로젝트에 추가한 대표적인 의존성은 Web MVC, Lombok, Data JPA, Testcontainers이다. https://start.spring.io/ 에서 편하게 구축할 수 있다.

     

    도메인 설계

    @Entity
    @Getter
    @Setter
    @Table(name = "user")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        Long id;
        String name;
    
        protected User() {}
    
        private User(String name) {
            this.name = name;
        }
    
        public static User withName(String name) {
            return new User(name);
        }
    }

     

    레포지토리 설계

     

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {}

     

    컨트롤러 설계 (서비스 설계는 넘어가겠다!)

     

    @RequiredArgsConstructor
    @RestController
    public class UserController {
        private final UserRepository userRepository;
    
        @GetMapping("/users")
        public ResponseEntity<?> findAllUsers() {
            List<User> users = userRepository.findAll();
            return ResponseEntity.ok()
                    .body(users);
        }
    }

     

     

    그리고 가장 중요한 application.yaml 설정 파일이다. 별도의 "test"라는 프로필을 설정해 둔것을 눈여겨 보자.

     

    spring:
      jpa:
        database-platform: org.hibernate.dialect.MySQLDialect
        properties:
          hibernate:
            format_sql: true
        show-sql: true
    
    logging:
      level:
        org.hibernate.orm.jdbc.bind: trace
    
    ---
    
    spring:
      config:
        activate:
          on-profile: default
      jpa:
        hibernate:
          ddl-auto: create
    ---
    
    spring:
      config:
        activate:
          on-profile: test
    
      jpa:
        hibernate:
          ddl-auto: create

     

    이제 테스트 코드를 작성해보겠다. 테스트 모듈 아래에 다음과 같은 추상 클래스를 작성하자.

    추상 클래스를 작성해서 다른 단위 테스트 코드가 해당 추상 클래스를 확장 할 수 있도록 하기 위함이다.

    해당 코드를 보면, MySQL 이미지 명을 통해 MySQLContainer라는 이름의 객체를 생성하는 것을 볼 수 있다. 이미지 뿐 아니라 다양한 컨테이너 설정을 해당 객체를 통해 진행할 수 있다.

     

    @ActiveProfiles("test")
    public abstract class TestContainerSupport {
    
        private static final String MYSQL_IMAGE_NAME = "mysql:8";
    
        private static  MySQLContainer MY_SQL_CONTAINER = new MySQLContainer(MYSQL_IMAGE_NAME);
    
        static {
            MY_SQL_CONTAINER.start();
        }
    
        @DynamicPropertySource
        public static void overrideProps(DynamicPropertyRegistry registry) {
            registry.add("spring.datasource.driver-class-name", MY_SQL_CONTAINER::getDriverClassName);
            registry.add("spring.datasource.url", MY_SQL_CONTAINER::getJdbcUrl);
            registry.add("spring.datasource.username", MY_SQL_CONTAINER::getUsername);
            registry.add("spring.datasource.password", MY_SQL_CONTAINER::getPassword);
        }
    
    }

     

    이제, 추상 클래스를 확장하는 단위 테스트 코드를 작성해보면 다음과 같이 작성할 수 있게 된다.

     

    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    public class UserRepositoryTest extends TestContainerSupport {
    
        @Autowired
        private UserRepository userRepository;
    
        private final String userName = "test1";
    
        @Test
        void member_save_test() {
            User user = User.withName(userName);
            user = userRepository.save(user);
            Assertions.assertThat(user.getId()).isNotNull();
        }
    }

     

    위 코드를 보면 @DataJpaTest@AutoConfigureTestDatabase(...) 어노테이션이 명시되어 있는 걸 볼 수 있다.

     

    • @DataJpaTest를 명시한 이유는 UserRepository와 같은 JPA와 관련된 빈을 주입받기 위함이다.
    • @DataJpaTest를 테스트 코드에 명시하게 되면 인메모리 DB를 자동으로 사용하게 되는데, 자동 사용을 비활성화 하기 위해 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 를 작성해준다.

     

    마지막으로 도커가 로컬에 설치 되어 있는지 확인하고 테스트를 실행하면 잘 동작할 것이다 !!

     

    참고 자료

    Riiid 기술 블로그

     

    TestContainer 로 멱등성있는 integration test 환경 구축하기

    By 박성은

    medium.com

    티스토리 블로그

     

    spring boot container 기반 테스트

    spring boot application 테스트를 위해서 혹은 로컬에 개발 테스트 환경을 구축하기 위해서 3rd party 서비스를 로컬 환경에 설치하거나 embedded 서비스를 사용하여 테스트를 하는 경우가 많다. 이러한 작

    devel-repository.tistory.com

    댓글

Designed by Tistory.