본문 바로가기

카테고리 없음

[개발생각] CI/CD unittest를 위한 인프라 환경 구성의 방법

Embadded infrastructure환경을 구축하자. (어디서든 테스트코드가 동작하게 만들자, 생각만해도 편하네)

 

로컬에서 유닛테스트 코드를 작성하고, 로컬 도커에 설치된 인프라환경(mysql, kafka, redis등등)으로 붙어서,

유닛테스트를 돌리게 구성을 해놨었다.

참, 로컬에서 인프라환경을 도커로 구성하니, 유닛테스트하기도 좋기 참 편했다. (나름 만족!! ^^)

 

그러던중, 이제 마지막 단계, CI/CD로 배포가 되는 파이프라인에도 unittest를 동작하도록 하고,

모든 테스트가 성공할때, 빌드를 진행하려고 하니, 고민스러운 부분이 생겼다.

 

바로 unittest infra환경이다.

ci/cd가 독립적으로 인프라 환경을 구성하고, 해당 인프라 환경으로 접근하여, 유닛테스트가 실행되도록 하고싶었다.

(마치 로컬에서 로컬도커로 인프라 접속하듯이...)

 

확인해 보니,

다행스럽게, kotest에는 extension과 test시점에 대한 event listener를 활용할수있었다.

유닛테스트가 처음 구동되는 이벤트 (ProjectListener)를 구독하여,

해당 시점 (beforeProject)에 extension이 동작하게 구성하고,

extension기능 클래스에는 testContainer를 이용하여, 자체적으로 인프라 환경 도커를 띄우게하고,

테스트코드가 동작하도록 구성을 하였다.

 

object EmbeddedInfraExtensionCore  {
    lateinit var redisContainer: RedisContainer
        private set

    lateinit var s3Container: MinIOContainer
        private set

    lateinit var kafkaContainer: KafkaContainer
        private set

    const val REDIS_IMAGE = "redis:7.0.11"
    const val MINIO_IMAGE = "minio/minio:latest"
    const val KAFKA_IMAGE = "apache/kafka:3.7.0"

    val log = LoggerFactory.getLogger(EmbeddedInfraExtensionCore::class.java)!!



    fun beforeProject() {
        val unittestProperties = UnittestPropertiesLoader().load()

        redisContainer = RedisContainer(DockerImageName.parse(REDIS_IMAGE))
            .withExtraHost("localhost", "127.0.0.1")
            .withExposedPorts(6379)
            .waitingFor(
                Wait.forListeningPort()
            )
        redisContainer.portBindings = listOf("${unittestProperties.redisPort}:6379")
        redisContainer.start()
        log.info("*Redis started at ${redisContainer.host}:${redisContainer.firstMappedPort}")

        s3Container = MinIOContainer(DockerImageName.parse(MINIO_IMAGE))
            .withUserName(unittestProperties.minioUserName)
            .withPassword(unittestProperties.minioPassword)
            .waitingFor(
                Wait.forHttp("/minio/health/ready")
                    .forStatusCode(200)
            )
        s3Container.portBindings = listOf("${unittestProperties.minioPort}:9000", "${unittestProperties.minioConsolePort}:9001")
        s3Container.start()
        log.info("*Minio started at ${s3Container.host}:${s3Container.firstMappedPort}")

        kafkaContainer = KafkaContainer(DockerImageName.parse(KAFKA_IMAGE))
            .withStartupTimeout(Duration.ofMinutes(3))
        kafkaContainer.portBindings = listOf("${unittestProperties.kafkaPort}:9092")
        kafkaContainer.start()
        log.info("*Kafka started at ${kafkaContainer.host}:${kafkaContainer.firstMappedPort}")
    }

    fun afterProject() {
        redisContainer.stop()
        log.info("*Redis stopped")

        s3Container.stop()
        log.info("*Minio stopped")

        kafkaContainer.stop()
        log.info("*Kafka stopped")
    }
}

 

여기서 중요한것이 UnittestPropertiesLoader 클래스의 역할이다.

beforeProject시점은 아직 스프링빈이 활성화가 되지않은 시점이기에,

application-unintest.yml 등 설정 파일을 @Value나 Property등으로 wired해서 읽을수없다.

직접 읽어야 한다 !!! (ㅠㅠ 아쉬워)

 

해당 인프라의 포트, 어드민 정보등을 하드코딩해도 되지만, 설정관리와 일원화를 위하여,

resource아래의 application-unintest.yml를 읽어서 해당 설정값을 추출하고, 이 값을 이용하여,

Embedded 인프라 실행 환경을 구성하도록 하고 싶었다.

그 의도에 의해서,

UnittestPropertiesLoader를 하나 만들고, 직접 yml을 읽어서, 사용할 설정값을 뽑아내는 클래스를 작성하여 활용하였다.

 

이렇게 unittest를 위한, Embedded 인프라 환경을 구성을 하였다.

ci/cd에서 유닛테스트가 동작할때, 최초, 위 Extension이 구동되게 되고, TestContainer를 통해, 도커이미지가 로컬에 구성이 된다.

(redis, kafka, db등등)

그리고, unittest코드는 Embedded 인프라로 접속하여, 테스트코드가 동작하게 된다.

 

테스트 코드 또한 프로덕션 코드와 같이, 잘 관리하고 효율적인 코드를 유지해야한다고 생각한다.

위와 같이 구성을 함에 따라, 훨씬 테스트 코드를 사용함에 있어서 불필요한 걱정을 할 필요가 없어졌다.

 

자동화 만세!!