오늘 이야기 할 내용은 Testcontainers라이브러리로 integration 테스트를 해보도록 하자.

Testcontainers는 java 라이브러리로 (다른 언어도 존재는 함) 데이터베이스, 메시지 큐 또는 웹 서버와 같은 의존성이 있는 모듈에서 테스트 할 수 있게 도와주는 도구이다. 기본적으로는 docker 컨테이너 기반으로 동작하기에 docker가 설치는 되어 있어야 한다.

만약 docker가 설치 되어 있지 않다면 docker를 설치 해야 된다. 내부적으로는 도커의 이미지를 땡겨와 실행하기 때문이다.

Testcontainers 다양한 테스트 프레임워크를 지원한다. junit4 부터 junit5, Spock등 java 진영에서 주로 사용하는 테스트 프레임워크를 지원하니 만약 다른 프레임워크를 사용한다면 조금은 추가적인 작업이 필요로 할 수도 있다. 하지만 걱정할 필요는 없다. 테스트 프레임워크에 종속성이 없어도 충분히 사용은 가능하다.

사실은 지원한다는 것도 Testcontainers의 라이플사이클만 지원하는 정도이다. 도커의 실행과 종료 정도만 관여하기에 사용해도 되고 원하지 않는다면 사용하지 않아도 좋다.
만약 다른 테스트 프레임워크를 사용한다면 라이플사이클 정도만 추가 하면 된다.

그럼 한번 간단하게 사용해보자. 필자는 junit5를 사용했고 테스트로는 mongodb를 사용할 예정이다.

일단 적절하게 디펜더시를 받도록 하자.

// 기타 몽고 관련 및 테스트 관련 디펜더시

testImplementation("org.testcontainers:testcontainers:1.12.0")

위의 문법은 gradle kotlin dsl 이다.

기본 문법


import com.mongodb.MongoClient; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import org.bson.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; class JDefaultTestcontatinersTests { private GenericContainer mongoDbContainer = new GenericContainer("mongo:4.0.10"); @BeforeEach void setup() { mongoDbContainer.start(); } @Test void default_mongo_db_test() { int port = mongoDbContainer.getMappedPort(27017); MongoClient mongoClient = new MongoClient(mongoDbContainer.getContainerIpAddress(), port); MongoDatabase database = mongoClient.getDatabase("test"); MongoCollection<Document> collection = database.getCollection("users"); Document document = new Document("name", "wonwoo") .append("email", "test@test.com"); collection.insertOne(document); FindIterable<Document> documents = collection.find(); assertThat(documents).hasSize(1); documents.forEach((Consumer<? super Document>) it -> { assertThat(it.get("name")).isEqualTo("wonwoo"); assertThat(it.get("email")).isEqualTo("test@test.com"); }); } @BeforeEach void close() { mongoDbContainer.stop(); } }

GenericContainer(imageName) 의 생성자에는 docker image 명을 작성하면 된다. 필자는 몽고디비로 테스트하기 위해 mongo:4.0.10 라는 이미지를 사용했다.

실제로 외부 포트는 (몽고디비에 경우) 27017 로 열리지 않는다. 외부의 영향을 받지 않기 하기 위해 그런듯 싶다. 외부 포트를 가져오기 위해서는 getMappedPort(originalPort) 메서드를 사용해서 가져올 수 있다.

또한 GenericContainer 에는 docker 와 관련된 많은 메서드들이 있다.
예를들어 Environment, Command, Label, Network, dependsOn등 docker와 관련된 커멘드들을 사용할 수 있으니 필요하다면 사용해도 된다.


private GenericContainer mongoDbContainer = new GenericContainer("mongo:4.0.10") .withEnv("FOO", "BAR") .withCommand("command test") .withLabel("TEST","LABEL") .withNetwork(Network.newNetwork()) .dependsOn(new MongoDbContainer());

그 후에 해당 테스트를 진행 하면된다. 너무 간단하다. 사실 뭐 별거 없다.

junit5

junit5를 사용해서 작성도 해보자.


testImplementation("org.testcontainers:junit-jupiter:1.12.0")

testcontainers 에서 지원해주는 junit5를 디펜더시 받아야 한다. 이 또한 너무 간단하다.


/// 기타 import import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers class JJunit5TestContatinersTests { @Container private GenericContainer mongoDbContainer = new GenericContainer("mongo:4.0.10"); @Test void junit5_mongo_db_test() { int port = mongoDbContainer.getMappedPort(27017); MongoClient mongoClient = new MongoClient(mongoDbContainer.getContainerIpAddress(), port); MongoDatabase database = mongoClient.getDatabase("test"); //(대충 테스트한다는 내용) } }

@Testcontainers 어노테이션은 junit5의 Extension 클래스로 해당 @Container 어노테이션이 달린 컨테이너를 실행시키는 어노테이션이다.

@BeforeEach 같이 실행전에 start() 메서드를 실행 할 필요없이 사용하면 되는 그런 어노테이션이다.
junit4경우에는 @Rule 어노테이션을 사용하면 된다.


@Rule public GenericContainer mongoDbContainer = new GenericContainer("mongo:4.0.10");

docker compose

docker compose 파일도 지원한다.

version: '2'
services:
  redis:
    image: redis:3.2
    ports:
      - 6379:6379
    volumes:
      - ~/data/redis:/data

  mongo:
    image: mongo:4.0.10

    ports:
      - 27017:27017

적절하게 docker compose 파일을 작성후에 테스트도 가능하다. volumes을 사용한다면 특정한 공간에 데이터가 저장되어 데이터가 계속 쌓이게 되므로 유의해야 한다.

@Testcontainers
class JDockerComposeTests {

    private final int REDIS_PORT = 6379;
    private final int MONGO_PORT = 27017;

    @Container
    private final DockerComposeContainer dockerCompose = new DockerComposeContainer(new File("src/test/resources/docker-compose.yaml"))
            .withExposedService("redis_1", REDIS_PORT)
            .withExposedService("mongo_1", MONGO_PORT);


    @Test
    void docker_compose_test() {

        int port = dockerCompose.getServicePort("mongo_1", MONGO_PORT);
        String host = dockerCompose.getServiceHost("mongo_1", MONGO_PORT);

        //(대충 테스트 한다는 내용)
    }

}

위와 같이 docker compose 파일을 작성후에 DockerComposeContainer 클래스를 이용해서 사용하면 된다. 사용법은 기존과 비슷하다.
참고로 꼭 파일명은 docker-compose 일 필요는 없다. 필자는 docker-compose는 docker-compose 라는 파일명이 익숙해서 그렇게 작성하였다.

spring boot data mongodb

기본적으로 사용법은 배워 봤다. 사실 그리 어렵지 않다. 적당한 디펜더시와 docker의 이미지만 설정한다면 쉽게 사용할 수 있다.

이번에는 우리가 자주 사용하는 spring boot를 사용해서 테스트를 진행 할 것이다. Spring boot 경우에는 일반적으로 자동설정에 있는 몽고 설정을 사용을 한다. 그래서 위와는 조금 다르게 설정을 해야 한다. 한번 살펴보자.

@DataMongoTest
@ContextConfiguration(initializers = MongoDbContainerInitializer.class)
class JSpringDataTestcontatinersTests {

    private final TodoRepository todoRepository;

    @Autowired
    private JSpringDataTestcontatinersTests(TodoRepository todoRepository) {

        this.todoRepository = todoRepository;
    }


    @Test
    void spring_data_mono_test() {

        todoRepository.save(new Todo(null, "wonwoo", "test@test.com"));

        List<Todo> todo = todoRepository.findAll();

        assertThat(todo).hasSize(1);

        todo.forEach(it -> {

            assertThat(it.getName()).isEqualTo("wonwoo");
            assertThat(it.getEmail()).isEqualTo("test@test.com");

        });

    }

}

필자는 @DataMongoTest 어노테이션을 이용해서 테스트를 작성했다. 물론 다른 nosql이나 rdb의 경우도 비슷하게 작성하면 된다.

MongoDbContainerInitializer 라는 클래스가 눈에 띈다. 이건 필자가 작성한 코드이다. 사실 별거 없고 위와 비슷하게 GenericContainer 실행하는 클래스이다.

public class MongoDbContainerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private GenericContainer mongoDbContainer = new GenericContainer("mongo:4.0.10");

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        mongoDbContainer.start();

        TestPropertyValues.of(

                "spring.data.mongodb.uri=mongodb://" + mongoDbContainer.getContainerIpAddress() + ":" + mongoDbContainer.getMappedPort(27017) + "/test"

        ).applyTo(applicationContext);
    }
}


spring boot 의 자동설정인 spring.data.mongodb.uri이라는 프로퍼티에 해당 url을 끼워 넣기 위한 작업을 하는 클래스이다. 위에서 말했다시피 실제 외부 포트는 27017이 아니기 때문이다.

조금 더 간단하게 사용하기 위해 메타 어노테이션을 이용해도 된다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@DataMongoTest
@ContextConfiguration(initializers = MongoDbContainerInitializer.class)
@DirtiesContext //optional
public @interface JDataMongoIntegrationTest {


}

위와 같이 작성 후에 테스트를 해보자.

@JDataMongoIntegrationTest
class JSpringDataCustomizedTestcontatinersTests {

    private final TodoRepository todoRepository;

    @Autowired
    private JSpringDataCustomizedTestcontatinersTests(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    @Test
    void spring_data_mono_customized_test() {

        // (대충 테스트 한다는 내용)

    }

}

위와 같이 작성해도 문제 없이 잘 테스트가 진행 될 것이다.

오늘은 이렇게 Testcontainers의 몇가지 기능을 살펴봤다.

Testcontainers 에는 많은 기능이 있지만 기본적인 테스트를 하기 위한 정도로 작성하였다. 추후에 embedded 관련해서도 작성해보려고 한다.

필자의 경우에는 Mongo image를 직접 작성했지만 Testcontainers 에서 지원해주는 모듈들이 많다. 예를들어 MySQLContainer, PostgreSQLContainer, MSSQLServerContainer, Db2Container, CouchbaseContainer, Neo4jContainer, ElasticsearchContainer, KafkaContainer, RabbitMQContainer, MockServerContainer 등등 더 많은 컨테이너들을 기본적으로 지원해주고 있다. 적절한 디펜더시만 받으면 된다. (근데 왜 몽고는 없지..?)

Testcontainers 의 또 다른 장점(?)은 spring boot의 지원을 잘해주고 있다. 뭐 많은 이유가 있겠지만 가장 큰 이유는 해당 프로젝트에 피보탈 개발자분이 한분 계신다. 또한 Spring boot 도 Testcontainers 종종 사용해서 테스트를 진행하고 있다.

사실 원래는 코틀린으로 먼저 작성을 해서 예제의 클래스들이 모두 J~~로 시작한다. 아직 대부분의 개발자분들이 java에 익숙하기 때문에 자바로 추가적으로 작성하였다. 만약 코틀린에도 관심이 있다며 여기(github)에 자바와 코틀린 소스 모두 있으니 참고하면 되겠다.

아직 필자도 많은 부분을 알고 있지는 않다. 필자도 이제 테스트할 때 도입할 예정이라 기본적으로 부분부터 살펴봤다. 사실 이정도만 알아도 큰 문제는 되지 않아 보인다. 쓰다보면 괜찮아지겠지..