Testcontainers for Java 시작하기

Testcontainers for Java 시작하기

이 가이드에서는 Testcontainers for Java를 이용해 실제 PostgreSQL 인스턴스로 Java 애플리케이션을 만들고 데이터베이스 상호작용을 테스트하는 방법을 배워요.

출처: 문서

본문

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

  • Maven으로 Java 프로젝트 만들기
  • PostgreSQL에서 고객 레코드를 관리하는 CustomerService 구현하기
  • 실제 Postgres 데이터베이스로 Testcontainers를 사용해 통합 테스트 작성하기
  • 테스트를 실행하고 모든 게 잘 동작하는지 확인하기

사전 준비 (Prerequisites)

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

참고: Testcontainers가 처음이라면 Testcontainers 개요를 방문해 Testcontainers가 무엇이고 어떤 장점이 있는지 알아보는 걸 권장해요.

Java 프로젝트 만들기

선호하는 IDE에서 Maven으로 Java 프로젝트를 만들어요. 이 가이드는 Maven을 쓰지만, 선호한다면 Gradle을 써도 돼요.

pom.xml에 다음 의존성을 추가해요.

<dependencies>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.3</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.5.6</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

이렇게 하면 Postgres JDBC 드라이버, 로깅용 logback, 테스트용 JUnit 5, 그리고 JUnit 5 지원을 위한 최신 maven-surefire-plugin이 추가돼요.

비즈니스 로직 구현하기

Customer 레코드를 만들어요.

package com.testcontainers.demo;

public record Customer(Long id, String name) {}

JDBC 연결 매개변수를 보관하고 데이터베이스 Connection을 제공하는 DBConnectionProvider 클래스를 만들어요.

package com.testcontainers.demo;

import java.sql.Connection;
import java.sql.DriverManager;

class DBConnectionProvider {
    private final String url;
    private final String username;
    private final String password;

    public DBConnectionProvider(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    Connection getConnection() {
        try {
            return DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

CustomerService 클래스를 만들어요.

package com.testcontainers.demo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class CustomerService {
    private final DBConnectionProvider connectionProvider;

    public CustomerService(DBConnectionProvider connectionProvider) {
        this.connectionProvider = connectionProvider;
        createCustomersTableIfNotExists();
    }

    public void createCustomer(Customer customer) {
        try (Connection conn = this.connectionProvider.getConnection()) {
            PreparedStatement pstmt = conn.prepareStatement("insert into customers(id,name) values(?,?)");
            pstmt.setLong(1, customer.id());
            pstmt.setString(2, customer.name());
            pstmt.execute();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public List<Customer> getAllCustomers() {
        List<Customer> customers = new ArrayList<>();
        try (Connection conn = this.connectionProvider.getConnection()) {
            PreparedStatement pstmt = conn.prepareStatement("select id,name from customers");
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                long id = rs.getLong("id");
                String name = rs.getString("name");
                customers.add(new Customer(id, name));
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return customers;
    }

    private void createCustomersTableIfNotExists() {
        try (Connection conn = this.connectionProvider.getConnection()) {
            PreparedStatement pstmt = conn.prepareStatement("""
                create table if not exists customers (
                    id bigint not null,
                    name varchar not null,
                    primary key (id)
                )
                """);
            pstmt.execute();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

CustomerService가 하는 일을 살펴보면,

  • 생성자는 createCustomersTableIfNotExists()를 호출해서 테이블이 존재하도록 해요.
  • createCustomer()는 데이터베이스에 고객 레코드를 삽입해요.
  • getAllCustomers()는 customers 테이블의 모든 행을 가져와 Customer 객체 목록으로 반환해요.

Testcontainers로 테스트 작성하기

CustomerService 구현은 준비됐지만 테스트하려면 PostgreSQL 데이터베이스가 필요해요. Testcontainers를 사용하면 Docker 컨테이너에서 Postgres 데이터베이스를 띄우고 그 데이터베이스를 대상으로 테스트를 실행할 수 있어요.

Testcontainers 의존성 추가하기

pom.xml에 Testcontainers PostgreSQL 모듈을 테스트 의존성으로 추가해요.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-postgresql</artifactId>
    <version>2.0.4</version>
    <scope>test</scope>
</dependency>

애플리케이션이 Postgres 데이터베이스를 사용하기 때문에, Testcontainers Postgres 모듈은 컨테이너 관리를 위한 PostgreSQLContainer 클래스를 제공해요.

테스트 작성하기

src/test/java 아래에 CustomerServiceTest.java를 만들어요.

package com.testcontainers.demo;

import static org.junit.jupiter.api.Assertions.assertEquals;

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.testcontainers.postgresql.PostgreSQLContainer;

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

    CustomerService customerService;

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

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

    @BeforeEach
    void setUp() {
        DBConnectionProvider connectionProvider = new DBConnectionProvider(
                postgres.getJdbcUrl(),
                postgres.getUsername(),
                postgres.getPassword()
        );
        customerService = new CustomerService(connectionProvider);
    }

    @Test
    void shouldGetCustomers() {
        customerService.createCustomer(new Customer(1L, "George"));
        customerService.createCustomer(new Customer(2L, "John"));

        List<Customer> customers = customerService.getAllCustomers();

        assertEquals(2, customers.size());
    }
}

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

  • postgres:16-alpine Docker 이미지로 PostgreSQLContainer를 선언해요.
  • @BeforeAll 콜백은 어떤 테스트 메서드가 실행되기 전에 Postgres 컨테이너를 시작해요.
  • @BeforeEach 콜백은 컨테이너의 JDBC 연결 매개변수로 DBConnectionProvider를 만들고, 그다음 CustomerService를 만들어요.
  • CustomerService 생성자는 customers 테이블이 없으면 만들어줘요.
  • shouldGetCustomers()는 고객 레코드 2개를 삽입하고 모든 고객을 가져와 그 개수를 단언해요.
  • @AfterAll 콜백은 모든 테스트 메서드가 끝난 뒤 컨테이너를 중지해요.

테스트 실행과 다음 단계

Maven으로 테스트를 실행해요.

$ mvn test

로그에서 Testcontainers가 Postgres Docker 이미지를 Docker Hub에서 가져오고(로컬에 없을 경우), 컨테이너를 시작하고, 테스트를 실행하는 걸 볼 수 있어요. Testcontainers로 통합 테스트를 작성하는 건 IDE에서 실행할 수 있는 유닛 테스트를 작성하는 것과 비슷해요. 팀원이 이 프로젝트를 클론해도 자신의 머신에 Postgres를 설치할 필요 없이 테스트를 실행할 수 있답니다.

요약 (Summary)

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

Postgres 외에도 Testcontainers는 많은 SQL 데이터베이스, NoSQL 데이터베이스, 메시징 큐 등을 위한 전용 모듈을 제공해요. 테스트에 필요한 어떤 컨테이너화된 의존성이든 Testcontainers로 실행할 수 있어요.

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

더 읽어보기 (Further reading)

  • JUnit 5를 이용한 Testcontainers 컨테이너 수명주기 관리
  • 테스트를 위해 H2를 실제 데이터베이스로 교체하기
  • Java Spring Boot 프로젝트에서 Testcontainers 시작하기

더 알아보기 (Learn more)