1. TestContainers란?
예전에 프로젝트를 할 때 멱등성 있는 테스트를 구성하기 위해 테스트 DB를 따로 띄워 테스트를 실행했던 적이 있습니다. 그 때는 Testcontainers의 존재를 몰랐기에 Docker Compose로 테스트 DB를 띄워 테스트를 실행해줬습니다. 테스트 DB 컨테이너를 계속 띄워 놓기엔 때문에 컴퓨터 리소스 낭비도 심했기 때문에 통합 테스트를 실행해야할 때마다 테스트 DB 컨테이너를 띄워주고 테스트가 종료되면 컨테이너를 내리는 식으로 진행되었는데 정말 귀찮은 작업이었습니다.
이런 작업을 자동화해주는 Testcontainers입니다. 똑같이 Docker 환경을 사용하며 테스트가 실행될 때 실제 DB와 같이 돌아가는 DB 컨테이너를 띄워주고, 테스트가 종료되면 자동으로 컨테이너를 내립니다.
멱등성이란?
컴퓨터 과학에서 멱등성이 보장된다는 것은 연산을 여러 차례 적용해도 똑같은 결과가 보장된다는 뜻입니다. 그런데 테스트 DB와 실제 운영 DB가 다르다면 멱등성이 보장되는 테스트가 아닙니다. 환경이 다르기에 테스트가 실패할 가능성이 존재합니다.
2. Docker
위에서 언급했다시피 Testcontainers는 도커 환경에서 실행되기 때문에 로컬에서 실행하려면 Docker Deamon이 필요하기에 Docker를 설치해줘야 합니다.Testcontainers를 실행시키기 위해 Docker Desktop을 설치하는 하고 Testcontainers 추가적인 기능을 사용하기 위해 Docker Compose를 설치해주겠습니다.
2-1. Docker Desktop
Docker Desktop이라는 프로그램을 아래 링크에서 설치합니다.
링크: https://www.docker.com/products/docker-desktop/
Docker Desktop을 설치하면 자동으로 도커 명령어도 설치됩니다.
2-2. Docker Compose
도커 컴포즈의 설치는 docker.docs에 설명되어 있습니다.
- 도커 컴포즈를 설치해줍니다.
$ curl -SL https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
- 권한을 부여해줍니다.
$ sudo chmod +x /usr/local/bin/docker-compose
- docker-compose 커맨드가 실행되지 않는다면 심볼릭 링크를 지정해줍니다.
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
- 도커 컴포즈 커맨드를 사용해 도커 컴포즈가 잘 실행되는지 확인합니다.
$ docker-compose --version
3. Testcontainers 설정
3-1. 의존성 추가
Testcontainers의 공식문서의 guide 부분을 보면 의존성 설정에 대한 상세 설명이 나와 있습니다. 아래와 같이 gradle 의존성을 추가해주면 됩니다.
dependencies {
...
// Testcontainers
testImplementation "org.testcontainers:junit-jupiter:1.19.7"
testImplementation "org.testcontainers:postgresql:1.19.7"
...
}
3-2. 데이터베이스 모듈
Testcontainers에서는 수 많은 모듈을 지원합니다. 위의 예시에는 postgresql을 예시로 들었지만, 다른 데이터베이스를 사용할 수 있습니다. 예를 들어 MySQL 모듈을 사용하고 싶다면 공식문서에서 찾아 MySQL Testcontainers 의존성을 추가해줄 수 있습니다.
testImplementation "org.testcontainers:mysql:1.19.7"
4. Testcontainers를 사용하는 다양한 방법
Testcontainers를 사용하기에 앞서 로컬 환경이라면 Docker Desktop을 실행해줍니다. 도커 컨테이너를 실행할 수 있는 환경이 구성되어 있어야 Testcontainers가 정상적으로 동작할 수 있습니다.
이제 준비가 됐다면 아래 코드 예시를 통해 Testcontainers를 어떻게 사용할 수 있는지 살펴봅시다.
4-1. 특정 모듈 사용하기
Testcontainers의 의존성과 데이터베이스 모듈 의존성을 추가했다면 Testcontainers에서 지원하는 어노테이션과 DB 모듈 컨테이너를 사용할 수 있게 됩니다.
먼저 src/test/resources 디렉토리의 application.yaml 파일을 아래와 같이 구성해보겠습니다. 구성하는 방법은 공식문서에 설명되어 있습니다.
jdbc라는 키워드 뒤에 tc라는 키워드를 넣으면 호스트 이름, 포트 및 데이터베이스 이름이 무시되고 테스트 컨테이너를 알아서 찾아가게 만들 수 있습니다. 따라서 특이하게 host-less URI 라고 지칭되는 /// 를 사용합니다. tc 키워드를 가진 URL을 JDBC 드라이버가 사용할 수 있도록 org.testcontainers.jdbc.ContainerDatabaseDriver를 설정해줬습니다.
- src/test/resources/application.yaml
spring:
datasource:
url: jdbc:tc:postgresql:///testdb
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
@Testcontainer와 @Container는 Testcontainers의 junit-jupiter 의존성으로 추가된 기능으로 DB 컨테이너의 생성을 도와줍니다. 반면 PostgreSQLContainer는 Testcontainer의 Postgre Module 의존성으로 추가된 기능입니다.
- test code
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers // JUnit5 확장팩으로 테스트 클래스에 @Container를 사용한 필드를 찾아서 컨테이너 라이프사이클 관련 메소드를 실행해준다.
public class OrderServiceIntegrationTest {
@Autowired
OrderRepository orderRepository;
@Container // 인스턴스 필드에 사용하면 모든 테스트마다 컨테이너를 재시작하고, 스태틱 필드에 사용하면 클래스 내부 모든 테스트에서 동일한 컨테이너를 재사용한다.
private static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer().withDatabaseName("testdb");
...
}
4-2. Testcontainers에서 지원하지 않는 DB 사용하기
Testcontainers에서는 많은 데이터베이스 모듈을 지원하지만 지원하지 않는 모듈도 존재합니다. 여기서 Testcontainers는 도커 환경을 사용한다는 점을 주목해봅시다. 도커 환경을 사용하기 때문에 데이터베이스의 이미지 또한 DockerHub에서 가져와 컨테이너를 만듭니다.
GenericContainer 클래스를 사용하면 원하는 이미지를 가져와 커스터마이징된 테스트 컨테이너를 구성할 수 있습니다.
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
public class OrderServiceIntegrationTest {
@Autowired
OrderRepository orderRepository;
@Container
private static GenericContainer postgreSQLContainer = new GenericContainer("postgres")
.withExposedPorts(5432);
...
}
4-3. 도커 컴포즈를 사용해 Testcontainers 띄우기
Docker Compose 파일을 사용해 테스트 컨테이너를 띄울 수 있습니다. 위의 방식보다 시간이 조금 더 걸립니다.
먼저 도커 컴포즈 yaml 파일을 만듭니다. 여기서 주의할 점이 있습니다. 원래 docker-compose.yaml 파일을 구성할 때 port 매핑을 설정해주지만 Testcontinaers에서 사용할 docker-compose 파일에서는 port를 매핑해주지 않는 것이 좋습니다. 해당 포트를 사용하는 프로그램이 존재하면 테스트가 실행되지 않기 때문입니다. Testcontainers에서는 사용 가능한 포트를 자동으로 매핑해주기 때문에 굳이 포트 매핑을 명시할 필요가 없습니다.
- src/test/resources/docker-compose.yaml
version: "3.9"
services:
test-db:
image: postgres
ports:
- 5432
environment:
POSTGRES_PASSWORD: 1234
POSTGRES_USER: test
POSTGRES_DB: testdb
- test code
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
public class OrderServiceIntegrationTest {
@Autowired
OrderRepository orderRepository;
@Container
private static DockerComposeContainer postgreSQLContainer =
new DockerComposeContainer(new File("src/test/resources/docker-compose.yaml"));
...
}
도커 컴포즈를 사용하면 속도가 더 느린데 그 이유는 아래와 같이 추정됩니다. 도커 컴포즈를 사용하지 않는 방법(4-1, 4-2 방식)으로 테스트를 실행하면 testcontainers 컨테이너와 postgres 컨테이너만 있으면 됩니다.
반면 도커 컴포즈를 사용해 테스트 컨테이너를 띄우는 경우 도커 컴포즈 컨테이너가 추가적으로 실행되기 때문에 다른 방식보다 느립니다.
5. 결론
Testcontainers는 확실히 느립니다. 메모리를 사용하는 H2 In-Memory DB와는 다르게 일반 DB는 디스크를 거쳐야하기에 느릴 수 밖에 없습니다. 게다가 Testcontainers는 컨테이너를 띄우는 시간도 필요하기에 시간이 더 걸립니다.
하지만 빠르다는 이유만으로 섣불리 H2 In-memory DB를 사용했다가 실제 운영 환경에서는 동작하지 않거나 SQL 함수 호환 문제로 인해 테스트가 실패해버리는 경우가 발생할 수 있습니다.
Testcontainers를 더 빠르게 사용하는 방법을 사용하면 충분히 개선이 가능하다고 합니다. Testcontainers를 사용하는 것이 멱등성 있는 테스트를 보장하기 가장 쉬운 방법이 될 수 있습니다.
관련 글
참고자료
- Testcontainers 공식문서
- 더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 강의
- Testcontainers 사용 기초
- TestContainer로 멱등성있는 integration test 환경 구축하기