Testcontainers
I. Testcontainers
TestContainers
는 통합 테스트를 지원하기 위해 개발된 오픈 소스 JAVA 라이브러리로, 도커 컨테이너를 활용해 다양한 스택에 대한 테스트 환경을 구축하고 관리할 수 있게 한다. MySQL
, Redis
같은 DBMS뿐만 아니라, Kafka
, ElasticSearch
등 50여가지의 다양한 스택을 지원한다. JUnit
과 연계도 잘 되어있기 때문에, 편하게 원하는 방향으로 의존성 및 환경을 구성할 수 있다. 도커 실행 과정에서 테스트 실행에 시간이 좀 더 걸리는 것은 슬프지만 어쩔 수 없긴하다.
(그림 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
구조로 구성하고, 비즈니스 로직을 기준으로 묶일 수 있는 도메인을 하나의 패키지로 묶어서 사용한다. 프로젝트의 대략적인 구조는 다음과 같다.
III. Testcontainers 적용
Testcontainers
사용을 위해선 Docker 환경이 필수적이다. 개인적으로 개발을 Window에서 진행한다면, WSL2
및 DockerDesktop
을 활용하는 게 가장 정신건강에 이로운 것 같다.
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_CONTAINER
와 REDISSON_CONTAINER
로 나누어 생성했다.
3. MySQL
MySQL
은 Redis
와 달리 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
Redis
및 MySQL
컨테이너가 설정한대로 생성 및 실행된 것을 확인할 수 있다.
2. Docker Desktop
MySQL, Redis, ryuk 도커 이미지가 생성되었다. 이 때 ryuk는 spring 런타임에 따라 컨테이너를 실행 또는 중지한다.
컨테이너 이름이 왜 저런진 모르겠지만(;;) 잘 생성된 것을 확인할 수 있다.
3. 통합 테스트
JWT를 적용한 간단한 회원가입/로그인/로그아웃 과정을 MockMvc로 작성하고 실행한 결과이다.