포스트

Testcontainers

I. Testcontainers

TestContainersLogo (그림 I-1) Testcontainers

TestContainers는 통합 테스트를 지원하기 위해 개발된 오픈 소스 JAVA 라이브러리로, 도커 컨테이너를 활용해 다양한 스택에 대한 테스트 환경을 구축하고 관리할 수 있게 한다. MySQL, Redis 같은 DBMS뿐만 아니라, Kafka, ElasticSearch 등 50여가지의 다양한 스택을 지원한다. JUnit과 연계도 잘 되어있기 때문에, 편하게 원하는 방향으로 의존성 및 환경을 구성할 수 있다. 도커 실행 과정에서 테스트 실행에 시간이 좀 더 걸리는 것은 슬프지만 어쩔 수 없긴하다.

TestContainersStacks (그림 I-2) Testcontainers 지원 모듈 목록

II. 프로젝트 구성

1. Stack

프로젝트에 Testcontainers를 통해 테스트를 진행할 스택의 종류는 다음과 같다.

  • Redis
    • Redis for Lettuce: Json Web Token 방식의 인증/인가 구현 시, Refresh Token을 Hashes 자료형으로 저장하는 데 사용
    • Redis for Redisson: Redis 기반 분산락(Distibuted Lock) 구현을 위해 사용
  • MySQL
    • 각 도메인에서 사용되는 데이터 저장을 위해 사용
  • DynamoDB (예정)
    • Key, Value 형태로 Member 관련 Log를 저장하기 위해 사용
    • 이 때 저장된 정보는 Spring Batch를 통해 Elastic Search로 이전(Testcontainers Logstash 미지원)
  • ElasticSearch (예정)
    • Member 관련 Log 분석 및 검색 기능 구현을 위해 사용

2. Structure

Controller - Service - Repository 구조로 구성하고, 비즈니스 로직을 기준으로 묶일 수 있는 도메인을 하나의 패키지로 묶어서 사용한다. 프로젝트의 대략적인 구조는 다음과 같다.

ProjectStructure (그림 II-2-1) Project Structure

III. Testcontainers 적용

Testcontainers 사용을 위해선 Docker 환경이 필수적이다. 개인적으로 개발을 Window에서 진행한다면, WSL2DockerDesktop을 활용하는 게 가장 정신건강에 이로운 것 같다.

1. Dependency

Testcontainers 사용을 위해선 의존성 추가가 필수적이다. 대부분의 TestContainers 관련 예시에서 testImplementation으로 Testcontainers를 사용하지만, 이번 프로젝트는 로컬 환경에서 다양한 스택을 사용해보는 것이 목적이기 때문에 implementation을 사용했다.

1
implementation group: 'org.testcontainers', name: 'testcontainers', version: '1.19.6'

2. Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.example.presets.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;

@Profile("local")
@Configuration
public class LocalContainerLettuceConfig {
    private static final String REDIS_DOCKER_IMAGE = "redis:5.0.3-alpine";

    static { // InitializationBlock
        GenericContainer<?> LETTUCE_CONTAINER =
                new GenericContainer<>(DockerImageName.parse(REDIS_DOCKER_IMAGE))
                        .withExposedPorts(6379)
                        .withReuse(true);

        LETTUCE_CONTAINER.start();

        System.setProperty("redis.lettuce.host", LETTUCE_CONTAINER.getHost());
        System.setProperty("redis.lettuce.port", LETTUCE_CONTAINER.getMappedPort(6379).toString());
    }
}

@Profile("local")을 통해 해당 Configuration이 local Profile에서만 동작하도록 했다. InitializationBlock을 통해 Bean이 초기화될 때 LETTUCE_CONTAINER를 구동하고, Property를 생성된 LETTUCE_CONTAINER에 맞춰 수정한다.

Redis 관련 컨테이너의 경우 추후 각 라이브러리의 차이를 비교하기 위해 LETTUCE_CONTAINERREDISSON_CONTAINER로 나누어 생성했다.

3. MySQL

MySQLRedis와 달리 GenericContainer 객체를 통해 핸들링하기 쉽지 않기 때문에, MySQL 관련 의존성을 추가했다.

1
implementation "org.testcontainers:mysql:1.19.6"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.example.presets.config.local;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

@Profile("local")
@Configuration
public class MySqlContainerConfig {
    private static final String MYSQL_DOCKER_IMAGE = "mysql:8";

    static {
        MySQLContainer<?> MYSQL_CONTAINER =
                new MySQLContainer<>(DockerImageName.parse(MYSQL_DOCKER_IMAGE))
                        .withExposedPorts(3306)
                        .withReuse(true);

        MYSQL_CONTAINER.start();

        System.setProperty("spring.datasource.url", MYSQL_CONTAINER.getJdbcUrl());
        System.setProperty("spring.datasource.username", MYSQL_CONTAINER.getUsername());
        System.setProperty("spring.datasource.password", MYSQL_CONTAINER.getPassword());
        System.setProperty("spring.datasource.driver-class-name", MYSQL_CONTAINER.getJdbcDriverInstance().getClass().getName());
    }
}

마찬가지로 @Profile("local")을 통해 local Profile에서만 동작하도록 하고, setProperty를 통해 생성된 컨테이너에 맞춰 Property를 수정했다.

IV. Result

1. Spring

SpringRun (그림 IV-1-1) Spring Run

RedisMySQL 컨테이너가 설정한대로 생성 및 실행된 것을 확인할 수 있다.

2. Docker Desktop

DockerImages (그림 IV-2-1) Docker Image

MySQL, Redis, ryuk 도커 이미지가 생성되었다. 이 때 ryuk는 spring 런타임에 따라 컨테이너를 실행 또는 중지한다.

DockerContainers (그림 IV-2-2) Docker Container

컨테이너 이름이 왜 저런진 모르겠지만(;;) 잘 생성된 것을 확인할 수 있다.

3. 통합 테스트

JWT를 적용한 간단한 회원가입/로그인/로그아웃 과정을 MockMvc로 작성하고 실행한 결과이다.

IntegrationTest (그림 IV-3-1) 통합 테스트 결과

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.