Testcontainers란?
Testcontainers는 Docker 컨테이너로 구동되는 실제 서비스와의 통합 테스트 환경을 자동으로 구성하고 시작하기 위해 쉽고 가벼운 API를 제공하는 테스트 라이브러리이다. Testcontainers를 사용하면 목(mock)이나 인메모리 서비스 없이 운영환경에서 사용하는 것과 동일한 유형의 서비스와 통신하는 테스트를 작성할 수 있다.
통합 테스트의 어려움 - Testcontainers를 사용해야 하는 이유
오늘날 소프트웨어 시스템은 다양한 기술과 도구를 활용하여 복잡한 비즈니스 문제를 해결한다. 대부분의 소프트웨어 시스템은 독립적으로 작동하지 않고 데이터베이스, 메시징 시스템, 캐시 서버 등 다른 서비스와 상호작용한다.
시스템끼리 서로 상호작용하는 이러한 환경은 애플리케이션 배포를 위한 테스트를 진행할 때 제약사항이 될 수 있다.
지속적 통합 및 지속적 배포(CI/CD) 프로세스에 의해 관리되는 애플리케이션의 배포에서 중요한 부분은 애플리케이션 동작의 정확성을 보장하는 자동화된 테스트이다.
단위 테스트(Unit Testing)는 데이터베이스나 메시징 시스템 같은 외부 서비스와 격리된 환경에서 비즈니스 로직과 구현 세부사항을 검증하는데 유용하다. 하지만 실제 애플리케이션 코드의 대부분은 이러한 외부 서비스와 밀접하게 연결되어 있다. 그렇기 때문에 애플리케이션의 신뢰성을 확보하려면 단위 테스트만으로는 충분하지 않으며, 애플리케이션의 전체적인 기능이 올바르게 작동하는지 확인할 수 있는 통합 테스트도 함께 작성해야 한다.
일반적으로 통합 테스트는 '통합 테스트 환경' 관리의 복잡성 때문에 어렵다고 여겨진다. 미리 구성된 인프라를 사용한 통합 테스트가 어려운 이유는 크게 두 가지이다. 첫째, 테스트 실행 전에 필요한 인프라가 정상적으로 구동되고 있는지, 그리고 데이터가 원하는 상태로 준비되어 있는지 매번 확인해야 한다. 둘째, 여러 빌드 파이프라인이 동시(병렬)에 실행될 때 한 테스트가 다른 테스트의 데이터를 변경하거나 영향을 줄 수 있어, 테스트 결과가 불안정해지거나 테스트 간 데이터 오염 등의 문제가 발생할 수 있다.
위의 문제들로 인해 일부 사람들은 통합 테스트를 위해 필요한 서비스의 인메모리 또는 임베디드 변형(별도의 서버 프로세스 없이 애플리케이션 내부에 직접 포함되어 실행되는 형태의 서비스)을 사용한다. 예를 들어, 애플리케이션이 PostgreSQL 데이터베이스를 사용한다면, 테스트를 위한 대체재로 H2 인메모리 데이터베이스를 사용하는 것이다. 물론 이는 통합 테스트를 전혀 작성하지 않는 것보다는 애플리케이션의 신뢰성을 증가시키지만, 외부 서비스의 목(Mock) 또는 인메모리 버전을 사용하는 것은 다음과 같은 문제를 가지고 있다.
인메모리 서비스는 프로덕션 서비스의 모든 기능을 가지지 않을 수 있다.
예를 들어, 애플리케이션에서 PostgreSQL 데이터베이스의 고급기능을 사용하는데 반해 H2는 통합 테스트에 사용하기 위해 이러한 모든 기능을 지원하지 않을 수 있다. 이는 단순히 기능을 지원하지 않는다는 문제를 넘어서, 새로운 기능 도입 시 테스트 환경의 제약으로 인해 해당 기능을 포기하게 만드는 상황까지 초래할 수 있다.
인메모리 서비스는 피드백 사이클을 지연시킨다.
예를 들어, 작성된 SQL 쿼리가 H2 인메모리 데이터베이스로 테스트할 때는 잘 작동하지만, 애플리케이션 배포 후 프로덕션 데이터베이스인 PostgreSQL에서는 작동하지 않을 수 있다. 이때, 작동하지 않는 쿼리 문제를 해결하기 위해 다른 구현을 적용해야 할 수도 있다. 이러한 상황은 '변경 사항에 대해 빠른 피드백을 제공한다'는 테스트의 본래 목적과 정반대의 결과를 낳는다.
Testcontainers의 동작 원리
처음에 이야기한 것처럼 Testcontainers 는 Docker 컨테이너로 래핑된 실제 서비스와의 통합 테스트를 위해 간단한 API를 제공하는 테스트 라이브러리며, Testcontainers를 사용하여 목(mock)이나 인메모리 서비스 없이 운영환경과 동일한 유형의 서비스와 통신하는 테스트를 진행할 수 있다.
Testcontainers에 기반한 통합 테스트는 다음의 단계로 진행된다.
Before Tests
- Testcontainers API를 사용하여 필요한 서비스(데이터베이스, 메시징 시스템 등)의 Docker 컨테이너를 시작한다.
- 컨테이너화된 서비스를 사용할 수 있도록 애플리케이션 구성을 설정하거나 업데이트한다.
During Tests
- 컨테이너화된 서비스를 사용하여 테스트가 실행된다.
After Tests
- 테스트의 성공 여부와 상관없이 Testcontainers는 사용된 서비스 컨테이너들을 파괴하는 작업을 진행한다.
Testcontainers 사용을 위해서는 Docker API와 호환되는 컨테이너 실행 엔진만 설치되어 있으면 된다. Docker Desktop이 설치되어 있다면 별도 설정 없이 바로 활용할 수 있다. (테스트 코드와 Docker 엔진을 연결하는 매개체라고 생각하면 된다.)
Testcontainers 사용방법 - Spring Boot
Testcontainers의 사용방법을 확인하기 위해 간단한 기능을 구현하고 이를 테스트해 본다.
1. 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.projectlombok:lombok'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation platform('org.testcontainers:testcontainers-bom:1.19.3')
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mariadb'
}
*BOM(Bill of Materials)은 Testcontainers와 관련된 여러 라이브러리들의 버전을 일괄 관리하는 도구이다.
덕분에 'org.testcontainers:testcontainers', 'org.testcontainers:junit-jupiter', 'org.testcontainers:mariadb'의 버전을 별도로 표기하지 않아도 된다.
1. 컨테이너 설정 및 테스트 코드 작성
테스트를 위해 필요한 엔티티와 레포지토리 클래스를 추가한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Fruit {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String color;
public Fruit(String name, String color) {
this.name = name;
this.color = color;
}
}
public interface FruitRepository extends JpaRepository<Fruit, Long> {
}
테스트 컨테이너를 활용할 수 있는 환경을 설정하고 과일(Fruit) 엔티티를 저장하는 기능이 정상 동작하는지 확인하는 테스트 코드를 작성한다.
@SpringBootTest
@Testcontainers
public class FruitRepositoryContainerTest {
@Autowired
FruitRepository fruitRepository;
@Container
static MariaDBContainer<?> mariadb = new MariaDBContainer<>("mariadb:latest")
.withDatabaseName("market")
.withUsername("tester")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mariadb::getJdbcUrl);
registry.add("spring.datasource.username", mariadb::getUsername);
registry.add("spring.datasource.password", mariadb::getPassword);
}
@Test
void save2() {
Fruit fruit = new Fruit("Banana", "yellow");
fruitRepository.save(fruit);
}
}
테스트 클래스에는 @Testcontainers 어노테이션을 추가하고, 사용할 컨테이너는 static 필드로 선언한 후 @Container 어노테이션을 붙인다.
Docker 엔진이 실행 중인 상태에서 테스트를 수행하면 컨테이너가 자동으로 생성되고, 테스트 완료 후 자동으로 삭제된다.
Sample Code
https://github.com/JaewookMun/programming-exercise/tree/main/spring-test/testcontainers
programming-exercise/spring-test/testcontainers at main · JaewookMun/programming-exercise
practice framework or skill such as spring, jpa, and so on - JaewookMun/programming-exercise
github.com
References
- What is TestContainers?
https://testcontainers.com/guides/introducing-testcontainers/
- TestContainers start
https://testcontainers.com/getting-started/
- Start with TestContainers in Spring Boot Project
https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/
- Spring Boot refereces of testing, 'Testcontainers'
https://docs.spring.io/spring-boot/reference/testing/testcontainers.html
'Spring' 카테고리의 다른 글
[Spring Boot] 환경변수 설정 - application.yml (Profile) (1) | 2025.05.11 |
---|---|
[Spring Framework] intellij에서 Spring legacy project 구성하기 (1) | 2024.09.07 |
[multipart/form-data] - MultipartFile과 JSON 함께 받기 (HttpMediaTypeNotSupportedException가 발생하는 이유) (0) | 2023.11.04 |
[Spring Security] session 동시 접속자 수 제한 (1) | 2023.04.16 |
[환경 설정] 데이터 베이스 (DB) (H2) (0) | 2023.01.29 |