Testcontainers로 Spring Boot REST API 테스트하기

Testcontainers로 Spring Boot REST API 테스트하기

이 가이드에서는 Spring Data JPA와 PostgreSQL을 사용하는 Spring Boot REST API를 만들고, Testcontainers와 REST Assured로 테스트하는 방법을 배워요.

출처: 문서

본문

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

  • REST API 엔드포인트를 가진 Spring Boot 애플리케이션 만들기
  • 데이터 저장과 조회에 Spring Data JPA와 PostgreSQL 사용하기
  • Testcontainers와 REST Assured로 REST API 테스트하기

사전 준비 (Prerequisites)

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

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

Spring Boot 프로젝트 만들기

Spring Initializr에서 Spring Web, Spring Data JPA, PostgreSQL Driver, Testcontainers 스타터를 선택해 Spring Boot 프로젝트를 만들어요. 또는 가이드 저장소를 클론해도 돼요. pom.xml의 핵심 의존성은 다음과 같아요.

<properties>
    <java.version>17</java.version>
    <testcontainers.version>2.0.4</testcontainers.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers-junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers-postgresql</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

모든 Testcontainers 모듈 의존성에 버전을 반복하지 않도록 Testcontainers BOM(Bill of Materials)을 사용하는 걸 권장해요.

JPA 엔티티 만들기

Customer.java를 만들어요.

package com.testcontainers.demo;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

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

    public Customer() {}

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

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

Spring Data JPA 리포지토리 만들기

package com.testcontainers.demo;

import org.springframework.data.jpa.repository.JpaRepository;

interface CustomerRepository extends JpaRepository<Customer, Long> {}

스키마 생성 스크립트 추가하기

src/main/resources/schema.sql을 만들어요.

create table if not exists customers (
    id bigserial not null,
    name varchar not null,
    email varchar not null,
    primary key (id),
    UNIQUE (email)
);

src/main/resources/application.properties에서 스키마 초기화를 활성화해요.

spring.sql.init.mode=always

REST API 엔드포인트 만들기

CustomerController.java를 만들어요.

package com.testcontainers.demo;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class CustomerController {
    private final CustomerRepository repo;

    CustomerController(CustomerRepository repo) {
        this.repo = repo;
    }

    @GetMapping("/api/customers")
    List<Customer> getAll() {
        return repo.findAll();
    }
}

Testcontainers로 테스트 작성하기

REST API를 테스트하려면 실행 중인 Postgres 데이터베이스와 시작된 Spring 컨텍스트가 필요해요. Testcontainers가 Docker 컨테이너에서 Postgres를 띄우고 @DynamicPropertySource가 이를 Spring에 연결해요.

CustomerControllerTest.java를 만들어요.

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.hasSize;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.List;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.postgresql.PostgreSQLContainer;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerTest {
    @LocalServerPort
    private Integer port;

    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16-alpine");

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    CustomerRepository customerRepository;

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = "http://localhost:" + port;
        customerRepository.deleteAll();
    }

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = List.of(
                new Customer(null, "John", "[email protected]"),
                new Customer(null, "Dennis", "[email protected]")
        );
        customerRepository.saveAll(customers);

        given()
            .contentType(ContentType.JSON)
            .when()
            .get("/api/customers")
            .then()
            .statusCode(200)
            .body(".", hasSize(2));
    }
}

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

  • @SpringBootTest는 임의의 포트에서 전체 애플리케이션을 시작해요.
  • PostgreSQLContainer가 @BeforeAll에서 시작되고 @AfterAll에서 중지돼요.
  • @DynamicPropertySource는 컨테이너의 JDBC URL, 사용자 이름, 비밀번호를 Spring에 등록해서 데이터소스가 테스트 컨테이너에 연결되게 해요.
  • @BeforeEach는 각 테스트 전에 모든 고객 행을 삭제해서 테스트 오염을 막아요.
  • shouldGetAllCustomers()는 고객 두 명을 삽입하고 GET /api/customers를 호출해 응답에 레코드 2개가 포함되는지 검증해요.

테스트 실행과 다음 단계

테스트를 실행해요.

$ ./mvnw test

또는 Gradle로,

$ ./gradlew test

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

요약 (Summary)

Testcontainers 라이브러리는 목(mock)이나 인메모리 데이터베이스 대신 프로덕션에서 쓰는 것과 같은 종류의 데이터베이스(Postgres)를 사용해 통합 테스트를 작성할 수 있게 도와줘요. 실제 서비스에 대해 테스트하기 때문에 코드를 리팩터링해도 애플리케이션이 예상대로 동작하는지 검증할 수 있답니다.

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

더 읽어보기 (Further reading)

  • Testcontainers JUnit 5 빠른 시작
  • Testcontainers Postgres 모듈
  • Testcontainers JDBC 지원

더 알아보기 (Learn more)