middlefitting

TestContainer, 도커 기반의 테스트 본문

Spring

TestContainer, 도커 기반의 테스트

middlefitting 2024. 1. 5. 01:28

로컬 테스트 코드를 작성하다 보면 꽤 곤란한 문제를 마주치게 된다. 보통 DB를 사용하는 서비스들이 많은데 DB에 대한 독립성 제공이 어려운 것이 그 이유이다. 테스트 용으로 로컬에 DB를 따로 만들어도 트랜잭션을 실수로 걸지 않는 등의 문제로 DB에 데이터가 들어가버리는 문제 등, 번거로운게 많다.

 

또한 로컬 환경에 의존하는 테스트는 CI 구축이 어려워진다. 예를 들어 mysql을 사용하려면 mysql이 설치되어 있어야 하지 않는가. CI로 많이 사용하는 깃허브 액션에서 mysql을 설치하고 테스트를 돌리는 것은 굉장히 비효율적으로 보인다. 그렇다고 인메모리 데이터베이스 h2를 통해 대체하는 것은 완전하지 않다. 

 

독립적이고, 환경에 종속적이지 않은 그런 테스트는 어떻게 할 수 있을까. 자연스럽게 도커가 떠오르게 된다. 도커만 설치되어 있으면 어디서나 테스트 코드를 돌릴 수 있으니 도커가 내장된 linux 환경에서 아주 쉽게 테스트를 돌릴 수 있다. 테스트 코드를 돌리기 전, 컨테이너를 만들고 빌드 이후 그것을 사용해서 테스트를 진행하는 것이다. 이를 통해 독립적이고 환경에 종속적인 테스트를 진행할 수 있다.

 

TestContainer는 이런 작업을 아주 쉽고, 더욱 유연하게 진행할 수 있게 도와준다. 직접 도커파일을 작성하는 것과 비교했을 때, 테스트 컨테이너가 가지는 장점은 다음과 같다. 코드에서도 컨테이너를 띄우거나 종료시킬 수 있고, 도커파일을 만들지 않아도 된다. 

 

Java, Go, Node.js, Python 등 매우 다양한 환경을 지원하기 때문에 해당 스택을 통해 프로젝트를 진행한다면 고려해볼 가치가 있다. 사용법에 대해서는 아래 공식문서를 참고하면 된다.

https://testcontainers.com/

 

Testcontainers

Testcontainers is an opensource framework for providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

testcontainers.com

 

 

테스트 컨테이너 사용 예제 Mysql

 

그렇다면 얼마나 편리한지 알아보자. Java에서 MySQL 테스트 컨테이너를 사용하려면 이렇게 작성하면 된다. 환경은 gradle, springboot이다.

testImplementation "org.testcontainers:testcontainers:1.19.3"
testImplementation "org.testcontainers:junit-jupiter:1.19.3"
testImplementation "org.testcontainers:mysql:1.19.3"

먼저 테스트 컨테이너에 필요한 의존성을 추가해주자 테스트 컨테이너 코어 라이브러리, Junit과의 통합을 위한 라이브러리 및 mysql용 라이브러리를 추가한다.

 

spring:
  datasource:
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
    url: jdbc:tc:mysql:8.0:///testdb?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: root
    password: 1234

그리고 yml 파일에는 다음고 같은 내용을 추가해주자

 

놀랍게도 끝났다. 이것만으로 mysql 기반의 테스트 컨테이너가 앞으로 실행될 것이다! 어떻게 이게 가능할까. 직접 도커파일을 작성하는 것보다 훨씬 쉽지 않은가. 굉장히 만족스럽다. 구동 원리는 JDBC 드라이버를 테스트 컨테이너를 사용하는 것으로 지정하였고, url에서 jdbc:tc:mysql 을 사용하도록 지정하였다. username과 password를 통해 아이디 비밀번호를 지정했는데 딱히 필요하지 않으면 지정하지 않고 바로 유저를 사용하는 것으로 url을 작성해도 된다. 이러면 똑똑하고 멋진 spring과 testcontainer 라이브러리가 티키타카를 하며 컨테이너를 잘 만들어준다. 

 

 

테스트 컨테이너 사용 예제 Mysql

 

Jdbc를 활용한 아주 쉬운 예시를 알아보았지만 Jdbc를 사용하지 않는 다른 컨테이너는 다른 문법을 사용해야 한다. Redis를 예시로 공식문서에서 제공하는 예시는 다음과 같다.

dependencies {
  testImplementation "com.redis:testcontainers-redis:2.0.1"
}

먼저 다음과 같이 의존성을 추가해준다.

 

https://testcontainers.com/modules/redis/

 

Testcontainers Redis Module

Start testing with real dependencies using the Redis Module for Testcontainers for Java, Go, and .NET

testcontainers.com

@Testcontainers
class RedisExampleTest {

  @Container
  private static RedisContainer container = new RedisContainer(
      RedisContainer.DEFAULT_IMAGE_NAME.withTag(RedisContainer.DEFAULT_TAG));

  @Test
  void testSomethingUsingLettuce() {
    String redisURI = container.getRedisURI();
    RedisClient client = RedisClient.create(redisURI);
    try (StatefulRedisConnection<String, String> connection = client.connect()) {
      RedisCommands<String, String> commands = connection.sync();
      Assertions.assertEquals("PONG", commands.ping());
    }
  }
}

그리고 이렇게 직접 테스트 코드에서 컨테이너를 만들어서 사용하면 된다.

 

그런데 이것만으로는 만족스럽지 않다. 직접 컨테이너를 테스트 코드마다 만들어야 하는 것은 불편하다. 그래서 해당 컨테이너를 만드는 컨텍스트를 만들어서 사용할수도 있다.

package com.gg.server.utils.config;

import com.redis.testcontainers.RedisContainer;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.utility.DockerImageName;

public class RedisInitializer implements
    ApplicationContextInitializer<ConfigurableApplicationContext> {
    private static final RedisContainer REDIS_CONTAINER =
        new RedisContainer(DockerImageName.parse("redis:5.0.3-alpine"))
            .withExposedPorts(6379);

    static {
        REDIS_CONTAINER.start();
    }

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        TestPropertyValues.of(
                "spring.redis.host=" + REDIS_CONTAINER.getHost(),
                "spring.redis.port=" + REDIS_CONTAINER.getMappedPort(6379)
        ).applyTo(applicationContext.getEnvironment());
    }
}

다음과 같이 ApplicationContextInitializer<ConfigurableApplicationContext>의 구현체, 컨텍스트를 만들어서 static 문법으로 start() 를 통해 초기화해준다. 그리고 PropertyValue에 추가해서 해당 내용에 필요한 호스트를 환경변수에 추가해주자.

그리고 다음과 같이 컨텍스트를 애노테이션으로 불러와서 사용하면 된다.

@ContextConfiguration(initializers = RedisInitializer.class)

 

 

 

이렇게 알아본 테스트 컨테이너는 독립적인 테스트 환경과, 유연성을 제공해주는 아주 유용한 라이브러리이다. 정말 많은 서비스들을 제공하던데 좀 더 DB 뿐만 아니라 다양한 서비스에 있어 유용한 도구가 된다.