조급하면 모래성이 될뿐

멀티모듈 + 테스트컨테이너 모듈 별 설정 분리 본문

구현 기록/TestContiners

멀티모듈 + 테스트컨테이너 모듈 별 설정 분리

Pawer0223 2023. 6. 26. 04:02

이전 포스팅에서 아래와 같은 구조로 테스트 환경을 구축하면서, 모듈 별 테스트 설정이 중복되는 것에 아쉬움이 남았다.

이 구조의 단점은 하나로 설정 가능한 정보가 API 모듈에 중복해서 관리된다는 것이었다.

  • API 모듈: MariaDB 접속을 위한 설정, Redis 접속을 위한 설정
  • Domain 모듈: MariaDB 접속을 위한 설정
  • Redis 모듈: Redis 접속을 위한 설정

최초 위와 같은 구조로 만든 이유는 다음과 같다.

  • 테스트 컨테이너에 의해 생성된 컨테이너의 포트는 고정적이지 않다.
  • 따라서 DB 접속에 필요한 정보를 코드로 동적으로 세팅해주어야 한다.
  • application.yml 파일에서 접속정보를 고정시킬 수 없다.
  • 즉, 런타임 시점에 코드로 설정정보를 주입해야 한다.
  • API 모듈에서, 다른 모듈(Domain, Infra)의 동적으로 설정정보를 주입하는 코드를 참조할 수 있는 방법을 찾지 못했다.

 

그런데 방법을 찾았다. 기존에 여러 모듈에 테스트 Fixture를 공유하기 위해 testFixtures 플러그인을 사용하고 있었는데...

testFixtures의 목적이 각 모듈의 테스트 과정에서 필요한 공통 클래스를 관리하기 위함이라고 이해했다.

그럼 각 모듈의 설정정보도 testFixture로 할 수 있지 않을까? 싶었다.

 

결과적으로 가능했고, 아래와 같은 구조가 되었다.

설정 과정

  • 각 모듈 별 테스트 컨테이너에 대한 설정과 Kotest를 통한 Global 설정을 하기 위해 의존성을 추가해 주었다. (build.gradle.kts)
    • 여기서 Kotest Global 설정은 프로젝트 레벨로 컨테이너를 실행, 종료하는 것을 의미로 사용했다.
    • 라이브러리 사용의 범위를 최소화하고자 testFixturesImplementation을 사용했다
testFixturesImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testFixturesImplementation("org.testcontainers:testcontainers:$testContainerVersion")
testFixturesImplementation("org.testcontainers:junit-jupiter:$testContainerVersion")
  • 각 모듈별 테스트 환경에 대한 컨테이너와, properties 정보를 testFixtures에 정의한다.
    • properties 정보를 분리한 이유는, 해당 모듈에 필요한 설정정보를 yml이 아닌 코드로 사용하기 위함이다.
    • 여기서 properties를 적용시키기 위해서 ContainerManager에서 참조하도록 했다. ContainerManager에서는 @AutoScan으로 프로젝트 레벨로 시작 전에 참조하고 있기 때문이다.
    • 예를 들면 아래와 같이 구성된다.
@AutoScan
object InfraContainerManager : BeforeProjectListener, AfterProjectListener {
    private val INFRA_PROPERTIES = InfraProperties
    private const val REDIS_PORT = 6379

    @Container
    private val redisContainer = GenericContainer(DockerImageName.parse("redis:alpine")).apply {
        withExposedPorts(REDIS_PORT)
    }

    override suspend fun beforeProject() {
        redisContainer.start()
        System.setProperty("spring.redis.host", redisContainer.host)
        System.setProperty("spring.redis.port", redisContainer.getMappedPort(REDIS_PORT).toString())
    }

    override suspend fun afterProject() {
        redisContainer.stop()
    }
}
object InfraProperties {
    init {
        System.setProperty("spring.mail.host", "smtp.gmail.com")
    }
}
  • API 모듈에서 필요한 정보는 application.yml에 정의한다.
    • 현재 프로젝트에서 API 모듈은, 다른 모듈을 참조하여 동작하는 최상위 모듈이기 때문에 yml파일로 사용했다.
  • API 모듈에서, 다른 모듈의 테스트 설정 정보가 필요한 경우 testFixtures를 참조한다.
    • build.gradle.kts 파일에 의존성을 추가해 주면 된다.
testImplementation(testFixtures(project(":domain")))
testImplementation(testFixtures(project(":infra")))

 

정리

결과적으로 테스트 환경에 대한 정보는 각 모듈별로 testFixtures 하위에서 관리된다. 따라서, 특정 모듈에서 다른 모듈의 테스트를 하기 위해서는 testFixtures만 의존하면 가능한 구조가 완성됐다.

 

모듈 별 테스트 환경에 대한 설정을 완벽하게 분리할 수 있다. 

 

물론 테스트 환경 정보를 코드로 작성해야 하는 단점도 있었다... 이 부분은 나중에 다른 방안을 찾게 되면 추가 포스팅해야 할 것 같다..

 

추가 정리 1 - 생성된 컨테이너의 포트가 고정적이지 않은 이유

생성된 컨테이너의 포트가 고정적이지 않은 이유는 공식문서에 다음과 같이 설명한다. 

Note that this exposed port number is from the perspective of the container.
From the host's perspective Testcontainers actually exposes this on a random free port. This is by design, to avoid port collisions that may arise with locally running software or in between parallel test runs.
Because there is this layer of indirection, it is necessary to ask Testcontainers for the actual mapped port at runtime. This can be done using the getMappedPort method, which takes the original (container) port as an argument:

 

정리하면 로컬에서 실행 중인 소프트웨어, 또는 병렬 테스트 실행 간에 발생할 수 있는 포트 충돌을 방지하기 위한 설계이다.

즉, 테스트 컨테이너가 생성될 때마다 mariadb나 redis 서비스의 port가 고정적이지 않다.

 

실제로 테스트 중간에 브레이크포인트를 걸고, 실행 중인 컨테이너의 포트정보를 확인해 보면 노출되는 포트는 동일하지만, 생성되는 포트가 계속 바뀌는 걸 확인할 수 있다.

 

참조

- 테스트 의존성 관리로 높은 품질의 테스트 코드 유지하기

반응형

'구현 기록 > TestContiners' 카테고리의 다른 글

Kotest + TestContainers Global 설정  (0) 2023.06.24
멀티모듈에서 통합테스트  (0) 2023.06.23
MariaDB TestContiner 적용  (0) 2023.06.23
TestContiners를 왜 썼는가  (0) 2023.06.23