테스트를 돌릴 때마다 애플리케이션이 새로 뜨고, DB 컨테이너를 수동으로 띄워야 한다면? Quarkus는 이 두 가지 문제를 어떻게 해결했을까?

@QuarkusTest — 앱을 한 번만 띄운다

Spring Boot 테스트에서는 @SpringBootTest를 붙인 테스트 클래스마다 ApplicationContext를 새로 로드하는 경우가 많습니다. 컨텍스트 캐싱이 되긴 하지만, 설정이 다르면 새 컨텍스트가 생기고 테스트 전체 실행 시간이 길어집니다.

Quarkus의 @QuarkusTest는 접근 방식이 다릅니다. 전체 테스트 스위트에서 애플리케이션을 딱 한 번만 시작 합니다.

JAVA
@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 BootQuarkus
앱 시작클래스/컨텍스트 단위전체 스위트에서 1번
HTTP 테스트MockMvc 또는 TestRestTemplateREST-assured 기본 내장
컨텍스트 캐싱설정별 캐싱 (불완전)단일 인스턴스 재사용
포트 설정@SpringBootTest(webEnvironment)자동으로 랜덤 포트

테스트 스위트 전체에서 앱이 한 번만 뜬다는 건, 테스트가 100개든 500개든 시작 비용이 동일하다는 뜻입니다. 테스트 수가 많아질수록 차이가 체감됩니다.


테스트에서 CDI 주입

@QuarkusTest 안에서는 CDI(Contexts and Dependency Injection)를 통해 빈을 주입받을 수 있습니다.

JAVA
@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 또는 네이티브 바이너리)를 실행 **해서 테스트합니다.

JAVA
@QuarkusIntegrationTest
public class TodoResourceIT {

    @Test
    public void testListEndpoint() {
        given()
            .when().get("/api/todos")
            .then()
            .statusCode(200);
    }
}

이 테스트가 중요한 이유는 다음과 같습니다.

  • ** 네이티브 이미지 검증 **: GraalVM 네이티브 컴파일 후 실제로 동작하는지 확인
  • ** 패키징 검증 **: uber-jar나 컨테이너 이미지가 정상적으로 동작하는지 확인
  • ** 블랙박스 테스트 **: 앱 내부에 접근하지 않고 HTTP 레벨에서만 테스트
BASH
# 네이티브 이미지 빌드 + 통합 테스트 실행
./mvnw verify -Dnative

@QuarkusIntegrationTest에서는 CDI 주입이 안 됩니다. 앱 프로세스가 별도로 실행되기 때문입니다. 오직 HTTP 요청으로만 테스트할 수 있습니다.


Mock — @InjectMock과 @QuarkusMock

외부 서비스나 복잡한 의존성을 가진 컴포넌트를 테스트할 때는 Mock이 필요합니다. Quarkus는 두 가지 방식을 제공합니다.

@InjectMock

테스트 클래스에서 CDI 빈을 자동으로 Mock으로 교체합니다.

JAVA
@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을 설정할 수도 있습니다.

JAVA
@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 컨테이너를 띄워줍니다 **.

PROPERTIES
# 이것만 설정하면 됩니다. URL, 포트, 사용자 모두 자동!
quarkus.datasource.db-kind=postgresql

DB URL을 명시하지 않으면 Quarkus가 알아서 다음을 수행합니다.

  1. Docker에서 PostgreSQL 컨테이너를 자동으로 시작
  2. 랜덤 포트를 할당하고 데이터소스 설정을 자동 주입
  3. 테스트가 끝나면 컨테이너 정리

지원하는 DevServices 목록

DevServices는 DB뿐만 아니라 다양한 인프라를 자동으로 띄워줍니다.

서비스컨테이너 이미지
PostgreSQLpostgres
MySQLmysql
MongoDBmongo
Kafkaredpanda (기본)
Redisredis
Elasticsearchelasticsearch
Keycloakkeycloak
RabbitMQrabbitmq
PROPERTIES
# 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를 관리할 필요가 없습니다.

PROPERTIES
# DevServices를 명시적으로 비활성화하려면
quarkus.devservices.enabled=false

# 특정 서비스만 비활성화
quarkus.datasource.devservices.enabled=false

Continuous Testing — 코드 저장 즉시 테스트

Quarkus Dev 모드(quarkus dev)에서는 ** 코드를 저장하면 관련 테스트가 자동으로 실행 **됩니다.

BASH
./mvnw quarkus:dev

Dev 모드가 실행되면 터미널 하단에 다음과 같은 프롬프트가 나타납니다.

PLAINTEXT
Tests paused
Press [r] to resume testing, [o] Toggle test output,
[h] for more options>

r을 누르면 Continuous Testing이 시작됩니다. 이후 코드를 수정할 때마다 다음이 발생합니다.

  1. 변경된 코드와 관련된 테스트를 자동으로 감지
  2. 해당 테스트만 즉시 실행
  3. 결과를 터미널에 실시간 표시

Spring Boot의 테스트 워크플로와 비교

PLAINTEXT
Spring Boot:
코드 수정 → IDE에서 테스트 수동 실행 → 컨텍스트 로드 대기 → 결과 확인

Quarkus Continuous Testing:
코드 수정 → 저장 → 자동으로 관련 테스트 실행 → 즉시 결과 표시

핫 리로드와 결합하면 코드 수정 → 테스트 통과 확인까지의 피드백 루프가 매우 짧아집니다. TDD를 할 때 특히 유용합니다.


테스트 필터링

특정 태그의 테스트만 실행하거나, 실패한 테스트만 재실행할 수도 있습니다.

PROPERTIES
# 특정 태그만 Continuous Testing에서 실행
quarkus.test.continuous-testing=enabled
quarkus.test.include-tags=fast
quarkus.test.exclude-tags=slow
JAVA
@Tag("fast")
@QuarkusTest
public class FastTest {
    @Test
    public void quickTest() {
        // 빠르게 실행되는 테스트
    }
}

@Tag("slow")
@QuarkusTest
public class SlowIntegrationTest {
    @Test
    public void heavyTest() {
        // 시간이 오래 걸리는 테스트
    }
}

테스트 프로파일

테스트 환경별로 다른 설정을 적용해야 할 때는 테스트 프로파일을 사용합니다.

JAVA
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);
    }
}
JAVA
@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의 테스트 환경이 얼마나 간소화되었는지 체감할 수 있을 것입니다.

댓글 로딩 중...