In-memory 왜써? TestContainer쓰자!

2024. 7. 11. 00:15Spring

What is TestContainer??


Testcontainers is a testing library that provides easy and lightweight APIs for bootstrapping integration tests with real services wrapped in Docker containers. Using Testcontainers, you can write tests talking to the same type of services you use in production without mocks or in-memory services.

번역

  • Testcontainers는 실제 서비스를 도커(Docker) 컨테이너로 감싸 부트스트래핑 통합 테스트를 위한 쉽고 가벼운 API를 제공하는 테스트 라이브러리입니다. Testcontainers를 사용하면 모의실험이나 인메모리 서비스 없이 프로덕션에서 사용하는 동일한 유형의 서비스와 대화하면서 테스트를 작성할 수 있습니다.

TestContainers 의 동작방식


  • 테스트 전
    • Testcontainers API를 사용하여 필요한 서비스(데이터베이스, 메시징 시스템 등) Docker 컨테이너를 시작합니다.
  • 테스트 중
    • 테스트는 이러한 컨테이너화된 서비스를 사용하여 실행한다.
  • 테스트 후
    • 테스트 컨테이너는 테스트가 성공적으로 실행되었는지 또는 테스트 실패가 있는지 여부에 관계없이 해당 컨테이너를 삭제한다.
      2024-07-11-00-10-43.png
  • 출처: https://testcontainers.com/guides/introducing-testcontainers/

1. Pain Point of Integration Test with Database


데이터베이스가 필요한 비즈니스 로직을 테스트하기 위해 주로 In-memory DB를 사용합니다.
In-memory는 다음과 같은 장정이 있습니다.

  1. 메모리 데이터베이스를 사용하기 때문에 실행속도가 빠르다.
  2. 테스트마다 메모리에 DB를 구성하고, 테스트가 종료될 때 삭제하므로 멱등성(idempotent)이 보장된다.
  3. 개발자의 로컬 PC 환경에서 테스트를 실행할 때 별도의 데이터베이스 연결이 필요하지 않는다.

메모리 데이터베이스는 만능?


  • 위에 말씀드린 것처럼 장점도 많지만, 아쉽게도 아닙니다.
    • 실제 사용하는 데이터베이스와 다르기 때문에 문제가 발생할 수 있습니다.
1. DB 엔진이 다르게 동작하기 때문에 실제 운영 데이터베이스와 다른 결과를 반환할 수 있다.
2. DB마다 다른 문법을 사용하기 때문에 대체 가능한 SQL 문법을 찾아야함.
    1. ANSI SQL처럼 표준 문법만으로 비즈니스 기능을 모두 커버하기 어려울 수 있음.
    2. JPQL( Java Persistence Query Lanager )처럼 추상화 된 문법을 사용하더라도 해결하기 어려울 수 있다.
3. DB 전용 내장 함수를 사용한다면 대체 가능한 함수를 찾아야함...
그럼 로컬 DB or 도커 컴포즈는 어떤가요?

  • 로컬 DB
    • 테스트가 끝난 후에 데이터가 남게되면 다음 실행 시 동일한 결과가 얻지 못할 수 있으므로 멱등성 관리가 어렵다.. ( 진짜 멱등성 관리 너무 힘들어요.. )
    • 개발자마다 로컬 데이터베이스에 관련된 설정이 다를 수 있기 때문에 통일된 설정 파일을 통해 관리하기 어렵움.. ( 어? 난 왜 안됨? → 로컬 DB 설정이 다를 확률 존재 )
    • CI/CD 파이프라인에서 사용할 수 없다.
    • 팀원이 채용 될 때 마다 설정 및 설치 해야하나..?
  • 도커 컴포즈
    • yml 파일에 필요한 이미지들을 명세하여 하나의 네트워크로 묶인 컨테이너(container) 그룹을 실행시켜야함..
    • 데이터베이스 컨테이너에 연결하여 테스트를 수행하고, 테스트가 종료되면 컨테이너를 정리합니다.
      • start, stop을 계속 신경써줘야함.
    • CI/CD 파이프라인에서 사용 가능합니다.

2. TestContainer


  • 도커 컴포즈는 로컬 DB, In-memory 중에서 최적의 선택지처럼 보이지만 더 간편하게 개선된 테스트 방법이 존재합니다.
  • TestContainer 는 도커 컴포즈처럼 테스트를 위한 컨테이너를 실행시키는 원리는 동일, 하지만 프로젝트 설정과 코드만으로 테스트를 위해 실행 가능!

여러가지 기능 제공


estcontainers for Java 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.

TestContainer는 단순하게 DB만 지원하지 않습니다. 쉬운 테스트를 위해 다양한 기능을 제공합니다.

- 웹 브라우저
    - 셀레니움
- 웹 서버 또는 프록시
    - nginx, 아파치
- NoSQL DB
- 로그 서비스
    - 카프카, 키바나

3. How to Use?


3.2 build.gradle

  • TestContainer 관련된 의존성을 추가한다.
    • org.testcontainers:mysql:1.19.1
    • org.testcontainers:testcontainers:1.19.1
    • org.testcontainers:junit-jupiter:1.19.1
dependencies {  
    ...

    // testContainer  
    testImplementation ("org.testcontainers:testcontainers:1.19.1")     // TC 의존성  
    testImplementation ("org.testcontainers:junit-jupiter:1.19.1")      // TC 의존성  
    testImplementation ("org.testcontainers:mysql:1.19.1")

    //rest-assured  
    testImplementation ("io.rest-assured:kotlin-extensions:5.3.2")  
}  

dependencyManagement {  
    imports {  
       mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}")  
    }  
}

3.2.1 application-test.yml

  • profile이 test 로 설정된 경우 사용되는 파일
  • 컨테이너 URL 정보를 제공
    • jdbc:tc:mysql:8.0.32:///{database_name}
  • 테스트 컨테이너에 접근할 때 사용하는 드라이버를 지정합니다.
    • ContainerDatabaseDriver를 사용하지만, 내부에서 MySQL 드라이버를 사용하기 때문에 커넥터 의존성이 필요합니다.
      spring:  
      datasource:  
      url: jdbc:tc:mysql:8.0.32:///katj  
      username: katj  
      password: katj123!  
      driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver  
      jpa:  
      hibernate:  
      ddl-auto: create-drop

번외

  • 코드레벨에서 DataBase를 설정할 수 도 있습니다.

    companion object {  
          @Container  
          private val mySQLContainer = MySQLContainer<Nothing>("mysql:latest").apply {  
              withDatabaseName("katj")  
              withUqsername("katj")  
              withPassword("katj123!")  
          }  
    
          @DynamicPropertySource  
          @JvmStatic        fun registerDynamicProperties(registry: DynamicPropertyRegistry) {  
              registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl)  
              registry.add("spring.datasource.username", mySQLContainer::getUsername)  
              registry.add("spring.datasource.password", mySQLContainer::getPassword)  
          }  
      }  

What is BaseTestEntity() ?

@TestInstance(TestInstance.Lifecycle.PER_CLASS)  
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)  
class BaseTestEntity()  
 {  

     @LocalServerPort private val port: Int? = null  
     @BeforeAll  
     fun init() {  
         RestAssured.port = port!!  
     }  

    companion object {  
        @Container  
        private val mySQLContainer = MySQLContainer<Nothing>("mysql:latest")  
    }  
}
  • TestContainer를 사용하기 위해서는 꼭 필요한 Base Class 입니다.
  • RestAssurd의 설정도 같이 포함되어있습니다.
  • @LocalServerPort 사용하면 @SpringBootTest 와 SpringBootTest.WebEnvironment.RANDOM_PORT 속성을 함께 사용할 때, 랜덤 포트로 서버가 실행될 때 사용된 포트를 가져올 수 있습니다..