Spring TestContainer 필요성과 설정하는 법(Mysql, Redis)

은찬·2023년 11월 21일
4

Spring TestContainer 필요성과 설정하는 법(Mysql, Redis)

TestContainer 란

TestContainerDocker 를 통해서 테스트 환경에 컨테이너를 띄우는 것이다

필요에 따라 Mysql , Redis 등 여러 컨테이너를 띄워서 원하는 테스트 환경을 구성할 수 있다

TestContainer 를 왜 써야하는가

Mysql, Redis 에서 왜 TestContainer 를 써야하는지 말해보겠다

Mysql

테스트 환경에서 DB 를 구성하는 방법은 여러가지가 있다.

  1. 실제 존재하는 DB 에 연결한다
  2. 임베디드 인메모리 DB 를 연결한다
  3. TestContainer 를 띄운다

1번은 단점이 너무 많다. 실제 DB 환경을 구성해야하므로 범용성이 낮고, Github ActionsJenkins 같은 환경에선 테스트가 어렵다

2번은 많이들 사용하는 방법이다. 실제로 나도 항상 2번 방법만 사용해왔다.
근데 최신 Mysql 은 임베디드 DB 를 지원하지 않는다. 그래서 많은 사람들이 H2 를 사용할 것이다.
근데 MysqlH2 둘은 다른 DB 이기 때문에 내부적으로 다르게 동작하는 부분들이 있어서 정확한 테스트라고 보기 어렵다

✨ 결국 3번으로 온다. TestContainerMysql 컨테이너를 띄우고 사용하면 실제 Mysql DB 환경에서 테스트를 돌릴 수 있고, 컨테이너를 즉각적으로 띄우고 내리기 때문에 사전 작업이 필요없다!! 👍

Redis

테스트 환경에서 Redis 를 구성하는 방법도 여러가지가 있다

  1. 실제 존재하는 Redis 에 연결한다
  2. Embedded Redis 라이브러리를 통해서 환경을 구성한다
  3. TestContainer 를 띄운다

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"

Mysql

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:///

Redis

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 를 통한 설정을 해줘야한다

이유는 다음과 같다

실제 RedisConfigspring.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 컨테이너를 사용하고 있다면 분명히 테스트를 실행할 때 아래처럼 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/

https://loosie.tistory.com/813

profile
`강한` 백엔드 개발자라고 해두겠습니다

1개의 댓글

comment-user-thumbnail
2024년 8월 16일

잘 보고 갑니다~!

답글 달기