Testcontainers로 Quarkus 애플리케이션 테스트하기

Testcontainers로 Quarkus 애플리케이션 테스트하기

이 가이드에서는 Hibernate ORM with Panache와 PostgreSQL을 사용하는 Quarkus REST API를 만들고, Quarkus Dev Services, Testcontainers, REST Assured로 테스트하는 방법을 배워요.

출처: 문서

본문

이 가이드를 통해 다음 내용을 배울 수 있어요.

  • REST API 엔드포인트를 가진 Quarkus 애플리케이션 만들기
  • 영속성에 Hibernate ORM with Panache와 PostgreSQL 사용하기
  • 테스트에서 내부적으로 Testcontainers를 사용하는 Quarkus Dev Services로 REST API 테스트하기
  • Dev Services가 지원하지 않는 서비스는 QuarkusTestResourceLifecycleManager로 테스트하기

사전 준비 (Prerequisites)

  • Java 17 이상
  • Maven 또는 Gradle
  • Testcontainers가 지원하는 Docker 환경

참고: Testcontainers가 처음이라면 Testcontainers 개요를 방문해 알아보는 걸 권장해요.

Quarkus 프로젝트 만들기

code.quarkus.io에서 RESTEasy Classic, RESTEasy Classic Jackson, Hibernate Validator, Hibernate ORM with Panache, JDBC Driver - PostgreSQL, Flyway 확장을 선택해 Quarkus 프로젝트를 만들어요. 또는 가이드 저장소를 클론해도 돼요. pom.xml의 핵심 의존성은 다음과 같아요.

<properties>
    <quarkus.platform.version>3.22.3</quarkus.platform.version>
</properties>

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-flyway</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-validator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-jackson</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

JPA 엔티티 만들기

Hibernate ORM with Panache는 JPA 사용을 단순화하기 위해 Active Record 패턴과 Repository 패턴을 지원해요. 이 가이드는 Active Record 패턴을 사용해요. PanacheEntity를 확장해 Customer.java를 만들어요. 이렇게 하면 persist(), listAll(), findById() 같은 내장 영속성 메서드를 엔티티가 갖게 돼요.

package com.testcontainers.demo;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
public class Customer extends PanacheEntity {
    @Column(nullable = false)
    public String name;

    @Column(nullable = false, unique = true)
    public String email;

    public Customer() {}

    public Customer(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

CustomerService CDI 빈 만들기

영속성 연산을 처리할 @ApplicationScoped와 @Transactional로 주석이 달린 CustomerService 클래스를 만들어요.

package com.testcontainers.demo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.List;

@ApplicationScoped
@Transactional
public class CustomerService {
    public List<Customer> getAll() {
        return Customer.listAll();
    }

    public Customer create(Customer customer) {
        customer.persist();
        return customer;
    }
}

Flyway 데이터베이스 마이그레이션 스크립트 추가하기

src/main/resources/db/migration/V1__init_database.sql을 만들어요.

create sequence customers_seq start with 1 increment by 50;

create table customers (
    id bigint DEFAULT nextval('customers_seq') not null,
    name varchar not null,
    email varchar not null,
    primary key (id)
);

insert into customers (name, email) values
    ('john', '[email protected]'),
    ('rambo', '[email protected]');

src/main/resources/application.properties에서 Flyway 마이그레이션을 활성화해요.

quarkus.flyway.migrate-at-start=true

REST API 엔드포인트 만들기

모든 고객을 가져오고 고객을 생성하는 엔드포인트가 있는 CustomerResource.java를 만들어요.

package com.testcontainers.demo;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;

@Path("/api/customers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CustomerResource {
    private final CustomerService customerService;

    public CustomerResource(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GET
    public List<Customer> getAllCustomers() {
        return customerService.getAll();
    }

    @POST
    public Response createCustomer(Customer customer) {
        var savedCustomer = customerService.create(customer);
        return Response.status(Response.Status.CREATED).entity(savedCustomer).build();
    }
}

Testcontainers로 테스트 작성하기

Quarkus Dev Services

Quarkus Dev Services는 개발 모드와 테스트 모드에서 구성되지 않은 서비스를 자동으로 프로비저닝해요. 확장을 포함하고 구성하지 않으면, Quarkus가 내부적으로 Testcontainers를 사용해 관련 서비스를 시작하고 애플리케이션이 그 서비스를 사용하도록 연결해줘요.

참고: Dev Services는 지원되는 Docker 환경이 필요해요.

Quarkus Dev Services는 SQL 데이터베이스, Kafka, RabbitMQ, Redis, MongoDB 같은 가장 흔히 쓰이는 서비스 대부분을 지원해요. 자세한 내용은 Quarkus Dev Services 가이드를 참고해요.

API 엔드포인트 테스트 작성하기

REST Assured로 GET /api/customers와 POST /api/customers 엔드포인트를 테스트해요. io.rest-assured:rest-assured 라이브러리는 프로젝트 생성 시 이미 테스트 의존성으로 추가됐어요. CustomerResourceTest.java를 만들고 @QuarkusTest로 주석을 답니다. 이렇게 하면 Dev Services를 사용해 필요한 서비스와 함께 애플리케이션이 부트스트랩돼요. 데이터소스 속성을 구성하지 않았으므로 Dev Services가 Testcontainers로 PostgreSQL 데이터베이스를 자동으로 시작해요.

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import java.util.List;

import org.junit.jupiter.api.Test;

@QuarkusTest
class CustomerResourceTest {

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = given()
            .when()
            .get("/api/customers")
            .then()
            .statusCode(200)
            .extract()
            .as(new TypeRef<>() {});
        assertFalse(customers.isEmpty());
    }

    @Test
    void shouldCreateCustomerSuccessfully() {
        Customer customer = new Customer(null, "John", "[email protected]");

        given()
            .contentType(ContentType.JSON)
            .body(customer)
            .when()
            .post("/api/customers")
            .then()
            .statusCode(201)
            .body("name", is("John"))
            .body("email", is("[email protected]"));
    }
}

테스트가 하는 일을 살펴보면,

  • @QuarkusTest는 Dev Services가 활성화된 전체 Quarkus 애플리케이션을 시작해요.
  • Dev Services는 Testcontainers로 PostgreSQL 컨테이너를 시작하고 데이터소스를 자동으로 구성해요.
  • shouldGetAllCustomers()는 GET /api/customers를 호출하고 Flyway 마이그레이션에서 시딩된 데이터가 반환되는지 검증해요.
  • shouldCreateCustomerSuccessfully()는 POST /api/customers 요청을 보내고 응답에 생성된 고객 데이터가 포함되는지 검증해요.

테스트 구성 맞춤 설정하기

기본적으로 Quarkus 테스트 인스턴스는 포트 8081에서 시작하고 postgres:14 Docker 이미지를 사용해요. src/main/resources/application.properties에 다음 속성을 추가해 둘 다 맞춤 설정해요.

quarkus.http.test-port=0
quarkus.datasource.devservices.image-name=postgres:15.2-alpine

quarkus.http.test-port=0으로 설정하면 애플리케이션이 즉시 사용 가능한 임의의 포트에서 시작해 포트 충돌을 피해요. devservices.image-name 속성은 프로덕션과 일치하는 특정 버전으로 PostgreSQL 이미지를 고정할 수 있게 해줘요.

Dev Services가 지원하지 않는 서비스 테스트하기

애플리케이션이 Dev Services가 기본 지원하지 않는 서비스를 사용할 수도 있어요. 그럴 때는 QuarkusTestResourceLifecycleManager를 사용해 테스트를 위해 Quarkus 애플리케이션이 시작되기 전에 그 서비스를 시작해요. 예를 들어 애플리케이션이 CockroachDB를 사용한다고 해볼게요.

먼저 CockroachDB Testcontainers 모듈 의존성을 추가해요.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>cockroachdb</artifactId>
    <scope>test</scope>
</dependency>

QuarkusTestResourceLifecycleManager를 구현하는 CockroachDBTestResource를 만들어요.

package com.testcontainers.demo;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.HashMap;
import java.util.Map;

import org.testcontainers.containers.CockroachContainer;

public class CockroachDBTestResource implements QuarkusTestResourceLifecycleManager {
    CockroachContainer cockroachdb;

    @Override
    public Map<String, String> start() {
        cockroachdb = new CockroachContainer("cockroachdb/cockroach:v22.2.0");
        cockroachdb.start();

        Map<String, String> conf = new HashMap<>();
        conf.put("quarkus.datasource.jdbc.url", cockroachdb.getJdbcUrl());
        conf.put("quarkus.datasource.username", cockroachdb.getUsername());
        conf.put("quarkus.datasource.password", cockroachdb.getPassword());
        return conf;
    }

    @Override
    public void stop() {
        cockroachdb.stop();
    }
}

테스트 클래스에서 @QuarkusTestResource와 함께 CockroachDBTestResource를 사용해요.

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertFalse;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import java.util.List;

import org.junit.jupiter.api.Test;

@QuarkusTest
@QuarkusTestResource(value = CockroachDBTestResource.class, restrictToAnnotatedClass = true)
class CockroachDBTest {
    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = given()
            .when()
            .get("/api/customers")
            .then()
            .statusCode(200)
            .extract()
            .as(new TypeRef<>() {});
        assertFalse(customers.isEmpty());
    }
}

restrictToAnnotatedClass = true 속성은 이 특정 테스트 클래스를 실행할 때만 CockroachDB 컨테이너가 시작되도록 보장해요. 모든 테스트에서 활성화되는 게 아니에요.

테스트 실행과 다음 단계

테스트를 실행해요.

$ ./mvnw test

또는 Gradle로,

$ ./gradlew test

PostgreSQL Docker 컨테이너가 시작되고 모든 테스트가 통과하는 걸 볼 수 있어요. 테스트가 끝나면 컨테이너는 자동으로 중지되고 제거돼요.

애플리케이션을 로컬에서 실행하기

Quarkus Dev Services는 개발 모드에서 구성되지 않은 서비스도 자동으로 프로비저닝해요. Quarkus 애플리케이션을 dev 모드로 시작해요.

$ ./mvnw compile quarkus:dev

또는 Gradle로,

$ ./gradlew quarkusDev

Dev Services가 PostgreSQL 컨테이너를 자동으로 시작해요. 시스템에 PostgreSQL 데이터베이스를 실행 중이고 그걸 사용하고 싶다면 src/main/resources/application.properties에 데이터소스 속성을 구성해요.

quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres

이 속성들을 명시적으로 설정하면 Dev Services는 데이터베이스 컨테이너를 프로비저닝하지 않고 구성된 데이터베이스에 연결해요.

요약 (Summary)

Quarkus Dev Services는 개발과 테스트 중에 Testcontainers를 사용해 필요한 서비스를 자동으로 프로비저닝함으로써 개발자 경험을 개선해요. 이 가이드에서 다룬 내용은,

  • JAX-RS와 Hibernate ORM with Panache로 REST API 구축하기
  • Dev Services가 데이터베이스 프로비저닝을 처리하면서 REST Assured로 API 엔드포인트 테스트하기
  • Dev Services가 지원하지 않는 서비스에는 QuarkusTestResourceLifecycleManager 사용하기
  • Dev Services로 애플리케이션을 로컬에서 실행하기

Testcontainers에 대해 더 알아보고 싶다면 Testcontainers 개요를 방문해요.

더 읽어보기 (Further reading)

  • Quarkus Dev Services 개요
  • Quarkus 테스트 가이드
  • Testcontainers Postgres 모듈

더 알아보기 (Learn more)