본문 바로가기

spring

Spring과 kotest에서 testContainer 사용 후기

자바와 스프링을 사용해서 테스트 컨테이너를 사용할 때는 @DynamicPropertySource를 이용해서 테스트 마다 동적으로 외부 시스템 연결 설정을 초기화해주는 방식을 사용해왔었다.

@SpringBootTest
@Testcontainers
public class ArticleLiveTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
      .withDatabaseName("prop")
      .withUsername("postgres")
      .withPassword("pass")
      .withExposedPorts(5432);

    @DynamicPropertySource
    static void registerPgProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", 
          () -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()));
        registry.add("spring.datasource.username", () -> "postgres");
        registry.add("spring.datasource.password", () -> "pass");
    }

    // tests are same as before
}

하지만 이번에 코틀린을 사용하게 되면서 해당 방식을 사용하는 것이 두가지 이유로 힘들었다.

첫번째는 junit과 kotest의 테스트 환경의 차이가 어려웠다. junit에서 사용할 때는, static 메서드를 통해서 컨테이너를 먼저 클래스 로딩 시점에 먼저 초기화를 진행하였지만 kotest에서 제공하는 테스트 스타일에서는 static 메서드를 활용하기 어려웠다.

두번재는 스프링 프로퍼티를 동적으로 초기화하는 방법이 어려웠다. 테스트 컨테이너를 가동하는 것은 관련 문서를 보면 어느정도 쉽게 따라할 수 있었지만 가동한 컨테이너를 설정을 동적으로 적용하는 방식에 대해서는 자료가 많이 없었다. kotest에서 spring extension과 testContainer extension을 지원하지만 두가지 방식을 혼용해서 사용하는 extension은 없었다.

@ContextConfiguration를 활용해서 property 동적으로 설정하기

스프링에서 지원하는 테스트 어노테이션 중에 하나로 테스트에 필요한 설정을 정의할 수 있는 어노테이션이다.
리소스의 위치를 XML 구성 파일이나 Groovy 스크립트를 통해서 정의할 수 있고 빈이나 설정 클래스를 설정할 수 있다. 그리고 마지막으로는 ApplicationContext를 초기화해주는 initializer를 통해서 context에 직접 접근할 수 있는 방식도 제공을 한다.

해당 어노테이션과 Spring에 적용하는 프로퍼티를 동적으로 주입하는 방식을 통해서 testContainer를 사용할 수 있었다.

다음은 Spring boot test와 함께 사용한 예제이다. kotest나 junit 테스트 클래스에 어노테이션을 통해서 관련 설정을 추가한다.

@SpringBootTest
@ContextConfiguration(initializers = [CustomContextInitializer::class])
class MySpringBootTest: DescribeSpec({
  // test code
})

커스텀 초기화 클래스는 ApplicationContextInitializer를 구현한 클래스를 구현해야한다.

해당 인터페이스를 보면 initialize 함수를 구현하도록 정의되어있다. 그 함수의 파라미터로 ApplicationContext를 입력받는데 바로 이 context가 스프링이 관리하는 context이고 해당 context에 원하는 설정을 주입하는 로직를 구현할 수 있다!

테스트 컨테이너를 가동하고 가동된 테스트 컨테이너의 설정을 반영하는 커스텀 초기화 클래스를 구현은 다음과 같이 했다.

abstract class ContainerContextInitializer(
  image: String,
  port: Int,
  vararg env: Pair<String, String>,
) : ApplicationContextInitializer<ConfigurableApplicationContext> {
  protected val container =
    GenericContainer(image)
      .withExposedPorts(port)
      .withEnv(mapOf(*env))

  private fun start() {
    this.container.start()
  }

  override fun initialize(applicationContext: ConfigurableApplicationContext) {
    start()
    val properties = this.properties()
    TestPropertyValues.of(properties).applyTo(applicationContext)
  }

  abstract fun properties(): Map<String, String>
}

커스텀 초기화 클래스를 이용한 mysql 설정

이전에 사용한 @DynamicPropertySource를 사용하면 테스트에 필요한 환경 단위로 구현을 해야하지만 이번 방식은 필요한 컨테이너를 하나씩 정의해서 필요한 컨테이너만 선택해서 사용할 수 있다는 점에서 더 좋다고 생각이 들었다.

참고 자료

@DynamicPropertySource를 사용하여 testContainer 설정
@ContextConfigration 공식 문서
적용한 프로젝트 코드