TestContainers: 테스트에서 도커 컨테이너를 실행할 수 있도록 하는 라이브러리
DB의 경우 DB마다 isolation, propagation 전략들이 다르다. (스프링에서는 DB에서 제공하는 이러한 전략들을 따를 뿐이다)
하지만 컨테이너의 테스트용 디비를 띄우고, 스크립트를 실행하고, 테스트가 끝난 후에는 컨테이너를 정리하는 등 복잡해질 수 있다.
이를 해결하기 위한 방법이 바로 TestContainers이다.
먼저 junit 확장체를 지원하는 testcontainers 모듈을 gradle에 추가한다.
여러 모듈 중에 현재 프로젝트에서 사용하고 있는 DB 모듈을 동일하게 사용하면 된다. 내 경우는 MariaDB를 사용하고 있음.
www.testcontainers.org/modules/databases/mariadb/
testImplementation "org.testcontainers:junit-jupiter:1.15.1"
testCompile "org.testcontainers:mariadb:1.15.1"
@ActiveProfiles("test")
@Testcontainers
@SpringBootTest
class ToyRepositoryTest {
@Autowired
private ToyRepository toyRepository;
@Autowired
private OrganizationRepository organizationRepository;
static MariaDBContainer mariaDB = new MariaDBContainer().withDatabaseName("toy_land_test");
@BeforeAll
static void beforeAll() {
mariaDB.start();
}
@AfterAll
static void afterAll() {
mariaDB.stop();
}
@Test
/* 테스트 */
}
spring:
jpa:
open-in-view: false
generate-ddl: true
show-sql: true
hibernate:
ddl-auto: create-drop
datasource:
url: jdbc:tc:mariadb:///toy_land_test
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
위 코드의 경우 테스트를 실행시키기 전에 컨테이너를 띄우고 실행 후에는 컨테이너를 없애는 것이 반복되어 느릴 수 있다.
아래 코드처럼 컨테이너를 static으로 띄워두고 테스트를 실행 시키기 전에 DB를 비우는 방식으로 코드를 짤 수도 있다. (위의 경우보다 빠를 것이다.)
package com.openhack.toyland.domain.toy;
import static org.assertj.core.api.Assertions.*;
import com.openhack.toyland.domain.Organization;
import com.openhack.toyland.domain.OrganizationRepository;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@ActiveProfiles("test")
@Testcontainers
@SpringBootTest
class ToyRepositoryTest {
@Autowired
private ToyRepository toyRepository;
@Autowired
private OrganizationRepository organizationRepository;
@Container
static MariaDBContainer mariaDB = new MariaDBContainer().withDatabaseName("toy_land_test");
@BeforeEach
void beforeEach() {
toyRepository.deleteAll();
organizationRepository.deleteAll();
}
@Test
/* 테스트 */
}
TestContainers가 제공하지 않은 일반적인 컨테이너를 만드는 방법
- GenericContainer(dockerImageName): 이미지 이름만 있으면 컨테이너를 만드는 것이 가능하다. 이 경우 먼저 로컬에서 이미지를 찾아보고, 로컬에 없으면 퍼블릭한 원격에서 찾아서 가져온다.
- withEnv: 환경 변수 세팅이 필요하다.
- withExposedPorts: 포트 노출 -> 일반적으로는 컨테이너가 랜덤하게 매핑해준다
- waitingFor: 해당 컨테이너가 띄워졌는지 확인 후 테스트 실행
TestContainers의 로그 살펴보기
- getLogs(): 모든 로그를 보는 방법
- followOutput(new Slf4jLogConsumer(log)): 로그 스트리밍
컨테이너 내부의 정보를 테스트 코드에서 활용하는 방법
ApplicationContextInitializer
스프링 ApplicationContext를 프로그래밍으로 초기화 할 때 사용할 수 있는 콜백 인터페이스로, 특정 프로파일을 활성화 하거나, 프로퍼티 소스를 추가하는 등의 작업을 할 수 있다.
TestPropertyValues
테스트용 프로퍼티 소스를 정의할 때 사용한다.
Environment
스프링 핵심 API로, 프로퍼티와 프로파일을 담당한다.
Testcontainer를 사용해서 생성한 컨테이너에 대해 ApplicationContextInitializer를 구현하여 생성된 컨테이너에서 정보를 Environment에 넣어준다.
그 다음 @ContextConfiguration을 사용해서 아까 구현했던 ApplicationContextInitializer 구현체를 등록해주면, 테스트 코드에서 Environment, @Value, @ConfigurationProperties 등 다양한 방법으로 프로퍼티를 사용할 수 있게 된다.
@ActiveProfiles("test")
@Testcontainers
@Slf4j
@ContextConfiguration(initializers = StudyServiceTest.ContainerPropertyInitializer.class)
class StudyServiceTest {
@Mock MemberService memberService;
@Autowired StudyRepository studyRepository;
@Value("${container.port}") int port;
@Container
static GenericContainer postgreSQLContainer = new GenericContainer("postgres")
.withExposedPorts(5432)
.withEnv("POSTGRES_DB", "studytest");
@BeforeAll
static void beforeAll() {
Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(log);
postgreSQLContainer.followOutput(logConsumer);
}
@BeforeEach
void beforeEach() {
System.out.println("===========");
System.out.println(postgreSQLContainer.getMappedPort(5432));
System.out.println(port);
studyRepository.deleteAll();
}
@Test
/* 테스트 */
static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
TestPropertyValues.of("container.port=" + postgreSQLContainer.getMappedPort(5432))
.applyTo(context.getEnvironment());
}
}
}
만약 테스트 컨테이너들을 여러개를 동시에 띄워서 사용해야 한다면? -> DockerComposeContainer
www.testcontainers.org/modules/docker_compose/
static DockerComposeContainer composeContainer =
new DockerComposeContainer(new File("src/test/resources/docker-compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
.withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));
위와 같이 docker-compose 파일을 주면 여러 컨테이너를 띄울 수 있다. (이때, 호스트쪽 포트 매핑은 안해두는 것이 낫다)
아직 컨테이너가 뜨지도 않았는데 테스트를 실행하는 것을 방지하기 위해 Wait.forListeningPort()를 추가 해 주는 것이 좋다.
위의 경우 docker-compose로 띄운 서비스의 정보를 참조하기 위해 구현하는 ApplicationContextInitializer는 아래와 같이 구현하면 된다.
static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
TestPropertyValues.of("container.port=" + composeContainer.getServicePort("study-db", 5432))
.applyTo(context.getEnvironment());
}
}
'Java (+ Spring)' 카테고리의 다른 글
운영 이슈 테스트: Chaos Monkey (0) | 2021.02.11 |
---|---|
Application의 성능 테스트하기: JMeter (0) | 2021.02.11 |
Mock객체로 테스트 하기: Mockito (0) | 2021.02.10 |
Java Unit Test: Junit5 (0) | 2021.02.10 |
Spring Boot: Spring Security & 카카오 로그인 API(...ing) (0) | 2020.10.06 |