TestContainer
란 Docker
를 통해서 테스트 환경에 컨테이너를 띄우는 것이다
필요에 따라 Mysql
, Redis
등 여러 컨테이너를 띄워서 원하는 테스트 환경을 구성할 수 있다
Mysql
, Redis
에서 왜 TestContainer
를 써야하는지 말해보겠다
테스트 환경에서 DB 를 구성하는 방법은 여러가지가 있다.
TestContainer
를 띄운다1번은 단점이 너무 많다. 실제 DB 환경을 구성해야하므로 범용성이 낮고, Github Actions
나 Jenkins
같은 환경에선 테스트가 어렵다
2번은 많이들 사용하는 방법이다. 실제로 나도 항상 2번 방법만 사용해왔다.
근데 최신 Mysql
은 임베디드 DB 를 지원하지 않는다. 그래서 많은 사람들이 H2 를 사용할 것이다.
근데 Mysql
과 H2
둘은 다른 DB 이기 때문에 내부적으로 다르게 동작하는 부분들이 있어서 정확한 테스트라고 보기 어렵다
✨ 결국 3번으로 온다.
TestContainer
로Mysql
컨테이너를 띄우고 사용하면 실제Mysql DB
환경에서 테스트를 돌릴 수 있고, 컨테이너를 즉각적으로 띄우고 내리기 때문에 사전 작업이 필요없다!! 👍
테스트 환경에서 Redis 를 구성하는 방법도 여러가지가 있다
1번은 위에 Mysql 과 비슷하다. 실제 Redis 환경을 구성해야해서 범용성이 낮고 번거롭다.
2번은 나도 사용했던 방식이다. 하지만 해당 오픈소스 라이브러리는 업데이트가 안된지 2년이 넘었고, 실제로 m1 칩을 쓰는 환경에서는 추가 설정이 필요하고, Spring Boot Context
가 종료돼도 Redis 서버가 종료가 안돼서 사용하는데 번거러움이 있었다.
✨
Redis
도 결국 최선책은 3번TestContainer
를 띄우는 것이다.TestContainer
를 띄우면 위 단점들을 모두 커버한다. 그냥TestContainer
를 쓰자
✨ 사용법을 설명하기 전에 주의할 점이 있는데
Docker
가 설치돼있어야한다. 당연히도 도커 컨테이너를 띄우는 것이기 때문에 필요하다
기본 의존성은 아래와 같다. 위 코드는 작성 기준 최신버전이다. 공식문서1 공식문서2 를 참고해서 최신 버전을 보자
testImplementation "org.testcontainers:testcontainers:1.19.2"
testImplementation "org.testcontainers:junit-jupiter:1.19.2"
testImplementation "org.testcontainers:mysql:1.19.2"
Mysql TestContainer
의존성을 추가한다.
그리고 아래 코드를 작성한다.
사용법은 해당 클래스를 상속받으면 된다
static
으로 구현한 이유는 매번 @Test
마다 테스트 인스턴스가 생성되는데 그럴 때마다 컨테이너를 올렸다 내렸다 하는데 비용일 줄이고자 싱글톤으로 만들었다
public abstract class TestContainerSupport {
private static final String MYSQL_IMAGE = "mysql:8";
private static final JdbcDatabaseContainer MYSQL;
static {
MYSQL = new MySQLContainer(MYSQL_IMAGE);
MYSQL.start();
}
}
프로퍼티는 아래처럼 설정하면 된다. /// 은 호스트와 DB 이름을 생략한 것이다
spring:
datasource:
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
url: jdbc:tc:mysql:8:///
public abstract class TestContainerSupport {
private static final String REDIS_IMAGE = "redis:latest";
private static final int REDIS_PORT = 6379;
private static final GenericContainer REDIS;
static {
REDIS = new GenericContainer(DockerImageName.parse(REDIS_IMAGE))
.withExposedPorts(REDIS_PORT)
.withReuse(true);
REDIS.start();
}
@DynamicPropertySource
public static void overrideProps(DynamicPropertyRegistry registry){
registry.add("spring.data.redis.host", REDIS::getHost);
registry.add("spring.data.redis.port", () -> String.valueOf(REDIS.getMappedPort(REDIS_PORT)));
}
}
Redis 는 퓨어한 GenericContainer
를 사용한다.
사용법은 Mysql
이랑 비슷하지만
@DynamicPropertySource
를 통한 설정을 해줘야한다
이유는 다음과 같다
실제 RedisConfig
는 spring.data.redis.host
프로퍼티 값을 통해서 동작하는데, 현재 TestContainer
를 통해서 띄운 Redis 의 host 와 port 는 미리 알 수 없기 때문에 동적으로 프로퍼티 값을 세팅해줘야한다
둘을 함께 쓰면 아래 코드다
public abstract class TestContainerSupport {
private static final String REDIS_IMAGE = "redis:latest";
private static final int REDIS_PORT = 6379;
private static final String MYSQL_IMAGE = "mysql:8";
private static final GenericContainer REDIS;
private static final JdbcDatabaseContainer MYSQL;
static {
REDIS = new GenericContainer(DockerImageName.parse(REDIS_IMAGE))
.withExposedPorts(REDIS_PORT)
.withReuse(true);
MYSQL = new MySQLContainer(MYSQL_IMAGE);
REDIS.start();
MYSQL.start();
}
@DynamicPropertySource
public static void overrideProps(DynamicPropertyRegistry registry){
registry.add("spring.data.redis.host", REDIS::getHost);
registry.add("spring.data.redis.port", () -> String.valueOf(REDIS.getMappedPort(REDIS_PORT)));
}
}
혹시 위 방법 그대로 MySQL 컨테이너를 사용하고 있다면 분명히 테스트를 실행할 때 아래처럼 mysql 컨테이너가 두개 뜰 것이다….
왜 그런지 설명을 하자면 TestContainerSupport
클래스에서 Mysql 컨테이너를 띄우고 applicaiton.yml 설정 파일을 통해서 mysql 컨테이너를 또 띄우는 것이다.
불필요한 테스트 컨테이너가 yml 파일로 띄워진 컨테이너는 싱글톤으로 관리가 안되기 때문에 낭비되는 리소스다
해결 방법을 간단하게 설명하자면 yml 파일에 존재하는 mysql 테스트 컨테이너 설정을 없애면 된다.
그렇다면 spring 에서 사용될 DataSource
는 어떻게 설정하는가? 🤔
위에서 사용한 @DynamicPropertySource
를 이용하면 된다
public abstract class TestContainerSupport {
private static final String MYSQL_IMAGE = "mysql:8";
private static final JdbcDatabaseContainer MYSQL;
static {
MYSQL = new MySQLContainer(MYSQL_IMAGE);
MYSQL.start();
}
@DynamicPropertySource
public static void overrideProps(DynamicPropertyRegistry registry){
registry.add("spring.datasource.driver-class-name", MYSQL::getDriverClassName);
registry.add("spring.datasource.url", MYSQL::getJdbcUrl);
registry.add("spring.datasource.~username", MYSQL::getUsername);
registry.add("spring.datasource.password", MYSQL::getPassword);
}
}
이렇게 Mysql 에 관한 @DynamicPropertySource
코드를 추가해서 테스트 컨테이너로 생성되는 DataSource
동적으로 할당해주면 된다.
그리고 yml 파일에 DataSource
설정은 지워준다
spring:
# datasource:
# driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
# url: jdbc:tc:mysql:8:///
이렇게 하고 테스트를 실행하면 mysql 컨테이너가 한개만 실행된다!! 👍
https://java.testcontainers.org/quickstart/junit_5_quickstart/
잘 보고 갑니다~!