Quarkus 테스팅 — @QuarkusTest와 DevServices로 빠른 테스트
테스트를 돌릴 때마다 애플리케이션이 새로 뜨고, DB 컨테이너를 수동으로 띄워야 한다면? Quarkus는 이 두 가지 문제를 어떻게 해결했을까?
@QuarkusTest — 앱을 한 번만 띄운다
Spring Boot 테스트에서는 @SpringBootTest를 붙인 테스트 클래스마다 ApplicationContext를 새로 로드하는 경우가 많습니다. 컨텍스트 캐싱이 되긴 하지만, 설정이 다르면 새 컨텍스트가 생기고 테스트 전체 실행 시간이 길어집니다.
Quarkus의 @QuarkusTest는 접근 방식이 다릅니다. 전체 테스트 스위트에서 애플리케이션을 딱 한 번만 시작 합니다.
@QuarkusTest
public class TodoResourceTest {
@Test
public void testListEndpoint() {
given()
.when().get("/api/todos")
.then()
.statusCode(200)
.body("$.size()", greaterThanOrEqualTo(0));
}
@Test
public void testCreateTodo() {
given()
.contentType(ContentType.JSON)
.body("{\"title\": \"테스트 할일\", \"completed\": false}")
.when().post("/api/todos")
.then()
.statusCode(201)
.body("title", equalTo("테스트 할일"));
}
}
REST-assured가 기본으로 포함되어 있어서 별도 의존성 없이 HTTP 테스트를 바로 작성할 수 있습니다.
Spring Boot 테스트와의 비교
| 구분 | Spring Boot | Quarkus |
|---|---|---|
| 앱 시작 | 클래스/컨텍스트 단위 | 전체 스위트에서 1번 |
| HTTP 테스트 | MockMvc 또는 TestRestTemplate | REST-assured 기본 내장 |
| 컨텍스트 캐싱 | 설정별 캐싱 (불완전) | 단일 인스턴스 재사용 |
| 포트 설정 | @SpringBootTest(webEnvironment) | 자동으로 랜덤 포트 |
테스트 스위트 전체에서 앱이 한 번만 뜬다는 건, 테스트가 100개든 500개든 시작 비용이 동일하다는 뜻입니다. 테스트 수가 많아질수록 차이가 체감됩니다.
테스트에서 CDI 주입
@QuarkusTest 안에서는 CDI(Contexts and Dependency Injection)를 통해 빈을 주입받을 수 있습니다.
@QuarkusTest
public class TodoServiceTest {
@Inject
TodoService todoService;
@Test
@Transactional
public void testCreateAndFind() {
Todo todo = new Todo();
todo.title = "테스트";
todoService.create(todo);
Todo found = todoService.findById(todo.id);
assertNotNull(found);
assertEquals("테스트", found.title);
}
}
Spring의 @Autowired와 같은 역할이지만, Jakarta CDI 표준의 @Inject를 사용합니다.
@QuarkusIntegrationTest — 네이티브 실행 파일 테스트
@QuarkusTest가 JVM 모드에서 앱을 띄워서 테스트한다면, @QuarkusIntegrationTest는 ** 이미 빌드된 아티팩트(JAR 또는 네이티브 바이너리)를 실행 **해서 테스트합니다.
@QuarkusIntegrationTest
public class TodoResourceIT {
@Test
public void testListEndpoint() {
given()
.when().get("/api/todos")
.then()
.statusCode(200);
}
}
이 테스트가 중요한 이유는 다음과 같습니다.
- ** 네이티브 이미지 검증 **: GraalVM 네이티브 컴파일 후 실제로 동작하는지 확인
- ** 패키징 검증 **: uber-jar나 컨테이너 이미지가 정상적으로 동작하는지 확인
- ** 블랙박스 테스트 **: 앱 내부에 접근하지 않고 HTTP 레벨에서만 테스트
# 네이티브 이미지 빌드 + 통합 테스트 실행
./mvnw verify -Dnative
@QuarkusIntegrationTest에서는 CDI 주입이 안 됩니다. 앱 프로세스가 별도로 실행되기 때문입니다. 오직 HTTP 요청으로만 테스트할 수 있습니다.
Mock — @InjectMock과 @QuarkusMock
외부 서비스나 복잡한 의존성을 가진 컴포넌트를 테스트할 때는 Mock이 필요합니다. Quarkus는 두 가지 방식을 제공합니다.
@InjectMock
테스트 클래스에서 CDI 빈을 자동으로 Mock으로 교체합니다.
@QuarkusTest
public class TodoResourceMockTest {
@InjectMock
ExternalNotificationService notificationService;
@Test
public void testCreateWithMockedNotification() {
// Mock 설정
Mockito.doNothing()
.when(notificationService)
.sendNotification(Mockito.anyString());
given()
.contentType(ContentType.JSON)
.body("{\"title\": \"알림 테스트\"}")
.when().post("/api/todos")
.then()
.statusCode(201);
// Mock 호출 검증
Mockito.verify(notificationService)
.sendNotification(Mockito.anyString());
}
}
Spring Boot의 @MockBean과 같은 역할입니다. 다만 Quarkus에서는 ** 컨텍스트를 새로 로드하지 않고** Mock을 주입합니다.
@QuarkusMock — 프로그래밍 방식
테스트 메서드 안에서 동적으로 Mock을 설정할 수도 있습니다.
@QuarkusTest
public class DynamicMockTest {
@Test
public void testWithDynamicMock() {
ExternalService mock = Mockito.mock(ExternalService.class);
Mockito.when(mock.getData()).thenReturn("mocked data");
QuarkusMock.installMockForType(mock, ExternalService.class);
// 이제 앱 내에서 ExternalService를 사용하는 모든 곳에서
// mock이 반환됩니다
given()
.when().get("/api/data")
.then()
.statusCode(200)
.body(equalTo("mocked data"));
}
}
DevServices — 테스트용 인프라 자동 프로비저닝
공부하면서 가장 인상 깊었던 기능이 바로 DevServices입니다.
application.properties에 데이터소스 URL을 설정하지 않으면, Quarkus가 ** 자동으로 Testcontainers를 사용해서 DB 컨테이너를 띄워줍니다 **.
# 이것만 설정하면 됩니다. URL, 포트, 사용자 모두 자동!
quarkus.datasource.db-kind=postgresql
DB URL을 명시하지 않으면 Quarkus가 알아서 다음을 수행합니다.
- Docker에서 PostgreSQL 컨테이너를 자동으로 시작
- 랜덤 포트를 할당하고 데이터소스 설정을 자동 주입
- 테스트가 끝나면 컨테이너 정리
지원하는 DevServices 목록
DevServices는 DB뿐만 아니라 다양한 인프라를 자동으로 띄워줍니다.
| 서비스 | 컨테이너 이미지 |
|---|---|
| PostgreSQL | postgres |
| MySQL | mysql |
| MongoDB | mongo |
| Kafka | redpanda (기본) |
| Redis | redis |
| Elasticsearch | elasticsearch |
| Keycloak | keycloak |
| RabbitMQ | rabbitmq |
# Kafka DevServices — 설정만 하면 자동으로 Redpanda 컨테이너 시작
quarkus.messaging.kafka.enabled=true
# Redis DevServices
quarkus.redis.hosts= # 비워두면 자동으로 컨테이너 시작
Spring Boot에서 Testcontainers를 사용하려면
@Container,@DynamicPropertySource등을 직접 설정해야 합니다. Quarkus DevServices는 이 과정을 ** 제로 설정으로 자동화 **합니다. 처음 써보면 "이게 된다고?" 하는 느낌을 받습니다.
DevServices 공유
같은 프로젝트의 여러 테스트 클래스가 같은 DevServices를 사용하면, ** 컨테이너를 공유 **합니다. 테스트마다 새 컨테이너를 띄우지 않습니다.
또한 quarkus dev 모드에서도 DevServices가 동작해서, 개발 중에도 별도로 Docker Compose를 관리할 필요가 없습니다.
# DevServices를 명시적으로 비활성화하려면
quarkus.devservices.enabled=false
# 특정 서비스만 비활성화
quarkus.datasource.devservices.enabled=false
Continuous Testing — 코드 저장 즉시 테스트
Quarkus Dev 모드(quarkus dev)에서는 ** 코드를 저장하면 관련 테스트가 자동으로 실행 **됩니다.
./mvnw quarkus:dev
Dev 모드가 실행되면 터미널 하단에 다음과 같은 프롬프트가 나타납니다.
Tests paused
Press [r] to resume testing, [o] Toggle test output,
[h] for more options>
r을 누르면 Continuous Testing이 시작됩니다. 이후 코드를 수정할 때마다 다음이 발생합니다.
- 변경된 코드와 관련된 테스트를 자동으로 감지
- 해당 테스트만 즉시 실행
- 결과를 터미널에 실시간 표시
Spring Boot의 테스트 워크플로와 비교
Spring Boot:
코드 수정 → IDE에서 테스트 수동 실행 → 컨텍스트 로드 대기 → 결과 확인
Quarkus Continuous Testing:
코드 수정 → 저장 → 자동으로 관련 테스트 실행 → 즉시 결과 표시
핫 리로드와 결합하면 코드 수정 → 테스트 통과 확인까지의 피드백 루프가 매우 짧아집니다. TDD를 할 때 특히 유용합니다.
테스트 필터링
특정 태그의 테스트만 실행하거나, 실패한 테스트만 재실행할 수도 있습니다.
# 특정 태그만 Continuous Testing에서 실행
quarkus.test.continuous-testing=enabled
quarkus.test.include-tags=fast
quarkus.test.exclude-tags=slow
@Tag("fast")
@QuarkusTest
public class FastTest {
@Test
public void quickTest() {
// 빠르게 실행되는 테스트
}
}
@Tag("slow")
@QuarkusTest
public class SlowIntegrationTest {
@Test
public void heavyTest() {
// 시간이 오래 걸리는 테스트
}
}
테스트 프로파일
테스트 환경별로 다른 설정을 적용해야 할 때는 테스트 프로파일을 사용합니다.
public class MockExternalServiceProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"external.service.url", "http://localhost:8888/mock",
"quarkus.log.level", "DEBUG"
);
}
@Override
public Set<Class<?>> getEnabledAlternatives() {
return Set.of(MockExternalService.class);
}
}
@QuarkusTest
@TestProfile(MockExternalServiceProfile.class)
public class ExternalServiceTest {
// MockExternalServiceProfile의 설정이 적용됩니다
}
같은 프로파일을 사용하는 테스트 클래스들은 하나의 앱 인스턴스를 공유합니다. 다른 프로파일을 사용하면 앱이 재시작됩니다. 프로파일 수를 최소화하는 것이 테스트 속도에 유리합니다.
정리
Quarkus의 테스트 인프라를 한마디로 요약하면 "설정은 최소화하고, 피드백은 최대화" 입니다.
- @QuarkusTest: 전체 스위트에서 앱 1번만 시작 → 테스트 속도 향상
- @QuarkusIntegrationTest: 네이티브 바이너리까지 검증 가능
- @InjectMock: 컨텍스트 재로드 없이 Mock 주입
- DevServices: DB, Kafka, Redis 등 테스트 인프라를 제로 설정으로 자동 프로비저닝
- Continuous Testing: 코드 저장 즉시 관련 테스트 자동 실행
Spring Boot에서 Testcontainers와 @MockBean을 설정하느라 보일러플레이트를 작성했던 경험이 있다면, Quarkus의 테스트 환경이 얼마나 간소화되었는지 체감할 수 있을 것입니다.