본문 바로가기
BackEnd/Project

[PharmNav] Ch05. Testcontainers

by 개발 Blog 2024. 9. 2.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

TestContainers 사용 이유

테스트 코드에서 JPA를 이용해 CRUD 테스트를 작성할 때, 어떤 DB 환경을 사용하는 것이 가장 좋을까? 아래의 선택지들이 있다.

  1. 운영환경과 유사한 스펙의 DB(개발 환경 DB) 사용하기
  2. 인메모리 DB(H2 등) 사용하기
  3. Docker를 이용하여 DB 환경 구성하기
  4. TestContainers 사용하기

TestContainers는 운영환경과 유사한 DB 스펙으로 독립적인 환경에서 테스트 코드를 작성할 수 있게 해 준다. TestContainers를 사용하면, 도커 컨테이너를 자동으로 관리해 주고, 운영 환경과 유사한 스펙으로 테스트를 할 수 있어 테스트 코드의 신뢰성을 높일 수 있다.

 

TestContainers 

TestContainers는 Java 코드만으로 Docker 컨테이너를 활용해 테스트 환경을 구성하는 도구이다. 이는 도커를 직접 관리해야 하는 번거로움을 해소해주며, 테스트 코드가 실행될 때 자동으로 도커 컨테이너를 실행하고, 테스트가 끝나면 자동으로 컨테이너를 종료 및 정리해 준다. TestContainers는 다양한 모듈을 지원하며, 이를 활용해 다양한 환경에서 테스트를 수행할 수 있다.

약국 도메인 예제

이전에 작성한 KakaoAddressSearchService에 대한 테스트 코드를 진행하기 위해, 약국 도메인을 만들고, 해당 도메인에 필요한 엔티티와 리포지토리를 생성한 후, Spock 프레임워크를 이용해 테스트 코드를 작성하는 방법을 설명한다.

  1. 약국 도메인을 만들고, 엔티티 및 리포지토리 패키지를 생성한다.
  2. 약국 엔티티를 작성하여, DB에 약국 정보를 저장할 수 있도록 한다.

약국 도메인 생성

먼저, Pharmacy 엔티티를 생성한다. 이 엔티티는 약국의 기본 정보를 포함하며, 데이터베이스에 저장될 수 있도록 구성된다.

package com.example.phamnav.pharmacy.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity(name = "pharmacy")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Pharmacy {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String pharmacyName;
    private String pharmacyAddress;
    private double latitude;
    private double longitude;

}
  • Pharmacy 엔티티는 @Entity 애너테이션을 사용하여 JPA 엔티티로 지정되며, 데이터베이스 테이블과 매핑된다.
  • @Id와 @GeneratedValue(strategy = GenerationType.IDENTITY)는 기본 키와 그 생성 전략을 나타낸다. 여기서는 자동 증가되는 ID 값을 사용한다.
  • 약국의 이름, 주소, 위도, 경도를 포함하는 필드를 가진다.

약국 리포지토리 생성

PharmacyRepository 인터페이스는 JpaRepository를 상속받아, Pharmacy 엔티티와 데이터베이스 간의 CRUD 작업을 처리할 수 있는 기본 메서드를 제공한다.

package com.example.phamnav.pharmacy.repository;

import com.example.phamnav.pharmacy.entity.Pharmacy;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PharmacyRepository extends JpaRepository<Pharmacy, Long> {

}
  • JpaRepository<Pharmacy, Long>는 기본적으로 findAll, save, delete 등의 메서드를 제공하며, 필요한 경우 커스텀 메서드도 추가할 수 있다.
  • 이 리포지토리는 Pharmacy 엔티티와 관련된 모든 데이터베이스 작업을 수행하는 역할을 한다.

Spock 프레임워크를 이용한 테스트 클래스 작성

PharmacyRepositoryTest 클래스는 Spock 프레임워크를 사용하여 통합 테스트를 수행한다. 이 클래스는 Spring의 @SpringBootTest 애너테이션을 사용해 스프링 컨텍스트를 로드하고, 필요한 빈을 주입받아 테스트를 수행한다.

package com.example.phamnav.pharmacy.repository

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification

@SpringBootTest
class PharmacyRepositoryTest extends Specification {
    @Autowired
    private PharmacyRepository pharmacyRepository

    def "test" (){

    }
}
  • @SpringBootTest: 이 애너테이션은 테스트에서 스프링 부트 애플리케이션 컨텍스트를 로드하여, 실제 애플리케이션 환경과 유사한 테스트 환경을 제공한다.
  • @Autowired: 스프링 빈을 자동으로 주입받아, 테스트에서 사용할 수 있게 한다.

이후, 통합 테스트 환경을 구성하고, 테스트 컨테이너를 활용해 DB와의 연동을 테스트한다.

 

TestContainers 통합 테스트 환경 구성

TestContainers를 활용하여 통합 테스트 환경을 구성하는 과정에 대해 설명한다. 이를 통해 실제 운영 환경과 유사한 조건에서 테스트를 수행할 수 있으며, 테스트의 신뢰성을 높이는 것이 가능하다.

 

공식문서 참조

테스트 환경을 구성할 때, 공식 문서를 참조하는 것이 중요하다. TestContainers는 다양한 모듈과 기능을 제공하며, 다음과 같은 문서를 참고할 수 있다.

 

Gradle 의존성 추가

TestContainers를 사용하기 위해 build.gradle 파일에 필요한 의존성을 추가한다.

// testcontainers
testImplementation 'org.testcontainers:spock:1.20.1'
testImplementation 'org.testcontainers:mariadb:1.20.1'
  • 이 의존성은 Spock 프레임워크와 MariaDB를 기반으로 한 TestContainers를 지원한다.

application.yml 설정

통합 테스트를 위해 application.yml 파일을 설정한다. 이 설정 파일에서는 MariaDB 컨테이너를 사용할 수 있도록 JDBC URL을 설정하고, Hibernate 관련 설정을 추가한다.

spring:
  datasource:
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
    url: jdbc:tc:mariadb:10:///
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

kakao:
  rest:
    api:
      key: ${KAKAO_REST_API_KEY}
  • jdbc:tc:mariadb:10:///: 이 URL 형식은 TestContainers가 MariaDB 컨테이너를 자동으로 생성하도록 한다.
  • ddl-auto: create: 테스트 시, 데이터베이스 스키마를 자동으로 생성한다.
  • show-sql: true: SQL 쿼리를 콘솔에 출력하여 디버깅을 쉽게 할 수 있다.

추상클래스 선언 및 테스트 작성

테스트에서 사용할 컨테이너를 관리하기 위해 추상클래스를 작성한다. 이 클래스는 테스트마다 컨테이너를 생성하고 종료하는 작업을 담당한다.

package com.example.phamnav

import org.springframework.boot.test.context.SpringBootTest
import org.testcontainers.containers.GenericContainer
import spock.lang.Specification

@SpringBootTest
class AbstractIntegrationContainerBaseTest extends Specification{
    static final GenericContainer MY_REDIS_CONTAINER

    static{
        MY_REDIS_CONTAINER = new GenericContainer<>("redis:7.4")
            .withExposedPorts(6379)

        MY_REDIS_CONTAINER.start()

        System.setProperty("spring.redis.host", MY_REDIS_CONTAINER.getHost())
        System.setProperty("spring.redis.port", MY_REDIS_CONTAINER.getMappedPort(6379).toString())
    }
}
  • GenericContainer: 특정 컨테이너를 생성하는 데 사용되며, 여기서는 Redis 컨테이너를 예시로 들고 있다.
  • start(): 컨테이너를 시작한다.
  • System.setProperty: Redis 컨테이너의 호스트와 포트를 시스템 속성에 설정하여, 테스트 환경에서 이를 사용할 수 있도록 한다.

테스트 클래스 작성

이제 실제 테스트를 수행할 클래스인 PharmacyRepositoryTest를 작성한다. 이 클래스는 약국 정보를 DB에 저장하고, 저장된 정보를 검증하는 테스트를 수행한다.

package com.example.phamnav.pharmacy.repository

import com.example.phamnav.AbstractIntegrationContainerBaseTest
import com.example.phamnav.pharmacy.entity.Pharmacy
import org.springframework.beans.factory.annotation.Autowired

class PharmacyRepositoryTest extends AbstractIntegrationContainerBaseTest {

    @Autowired
    private PharmacyRepository pharmacyRepository

    def "PharmacyRepository save"() {
        given:
        String address = "서울 특별시 성북구 종암동"
        String name = "은혜 약국"
        double latitude = 37.59
        double longitude = 127.03

        def pharmacy = Pharmacy.builder()
                .pharmacyAddress(address)
                .pharmacyName(name)
                .latitude(latitude)
                .longitude(longitude)
                .build()
        when:
        def result = pharmacyRepository.save(pharmacy)

        then:
        result.getPharmacyAddress() == address
        result.getPharmacyName() == name
        result.getLatitude() == latitude
        result.getLongitude() == longitude

    }
}
  • given: 테스트에 필요한 데이터를 준비한다. 이 경우, 약국의 주소, 이름, 위도, 경도를 설정한다.
  • when: 설정된 데이터를 데이터베이스에 저장하는 동작을 수행한다.
  • then: 저장된 데이터가 올바르게 DB에 저장되었는지 검증한다.

테스트 결과

이 테스트를 수행하면, MariaDB 컨테이너가 자동으로 시작되고, 테스트가 끝나면 자동으로 종료된다. MariaDB 컨테이너는 임의의 포트로 바인딩되며, application.yml에 설정된 대로 자동으로 연결된다. 이를 통해 실제 운영 환경과 유사한 조건에서 테스트를 수행할 수 있으며, 테스트 코드의 신뢰성을 크게 향상시킬 수 있다. 

테스트 결과, 테스트 컨테이너가 자동으로 MariaDB 컨테이너를 시작하고, 약국 정보를 DB에 저장하는 것을 확인할 수 있다. TestContainers를 사용하면 이렇게 실제 운영 환경과 유사한 스펙에서 테스트를 할 수 있어, 테스트 코드의 신뢰성을 높이고 예기치 못한 오류를 사전에 방지할 수 있다.