👾

Getting started with Testcontainers for Java をやってみる

2024/10/22に公開

はじめに

Testcontainers というのを知ったので試しに Java でやってみようと思います。

Testcontainers とは?

https://testcontainers.com/

公式では以下のように書かれています。

実際の依存関係を持つユニットテスト
Testcontainersは、データベース、メッセージブローカー、ウェブブラウザなど、Dockerコンテナ
で実行できるものなら何でも、使い捨ての軽量インスタンスを提供するためのオープンソースライブラリ
です。

また Getting started with Testcontainers for Java のページには以下のようの書かれています。
https://testcontainers.com/guides/getting-started-with-testcontainers-for-java/

Testcontainersは、Dockerコンテナにラッピングされた実際のサービスを使って統合テストを
ブートストラップするための、簡単で軽量なAPIを提供するテストライブラリです。
Testcontainersを使えば、モックやインメモリサービスを使わずに、本番で使うのと同じタイプの
サービスと対話するテストを書くことができます。

なんだか便利そうですね!

ということで Getting started with Testcontainers for Java を実際に書かれている通りにやってみましたが、本当に全部コピペでできてしまったので Postgres のところを MySQL にしてやってみたのでそのコードをメモしておきます!
使った MySQL のモジュールはこちらです。

https://testcontainers.com/modules/mysql/

では公式の順番で記載していきます。

Maven で Java プロジェクトを作成する

プロジェクトを作成したら pom.xml に以下を追加します。

    <dependencies>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mysql</artifactId>
            <version>1.20.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
            <scope>test</scope>
        </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>

MySQLデータベースに接続するためのドライバ、ロギングするための logback-classic、JUnit5でテストするための junit-jupiter を追加しました。 また、JUnit5 のテストをサポートするために、最新バージョンの maven-surefire-plugin を使用しています。
アプリケーションに MySQL データベースを使用しているので、Testcontainers MySQL モジュールをテストの依存関係として追加しました。

ビジネスロジックの実装

ここはほぼ同じです。

Customer.java
package org.example;

public record Customer(Long id, String name) {}
DBConnectionProvider.java
package org.example;

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.java
package org.example;

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(255) not null,
                        primary key (id)
                    )
                    """
            );
            pstmt.execute();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

ここの createCustomersTableIfNotExists メソッド内の varcharvarchar(255) に書き換えています。

Testcontainersを使ってテストを書く

MySQL に変更したバージョンです。
といっても、new を Postgres から MySQL に変えて、変数名を変えたくらいで簡単に変更できました!

CustomerServiceTest.java
package org.example;

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.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

class CustomerServiceTest {

    static MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"));

    CustomerService customerService;

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

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

    @BeforeEach
    void setUp() {
        DBConnectionProvider connectionProvider = new DBConnectionProvider(
                mysql.getJdbcUrl(),
                mysql.getUsername(),
                mysql.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());
    }
}

実行結果

実行結果はこうなりました。

% mvn clean test
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for org.example:TestcontainersForJava:jar:1.0-SNAPSHOT
[WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-compiler-plugin is missing. @ line 51, column 21
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO] 
[INFO] -----------------< org.example:TestcontainersForJava >------------------
[INFO] Building TestcontainersForJava 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[WARNING] The artifact mysql:mysql-connector-java:jar:8.0.33 has been relocated to com.mysql:mysql-connector-j:jar:8.0.33: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
[INFO] 
[INFO] --- clean:3.2.0:clean (default-clean) @ TestcontainersForJava ---
[INFO] Deleting /Users/takeuchi/Study/TestcontainersForJava/target
[INFO] 
[INFO] --- resources:3.3.1:resources (default-resources) @ TestcontainersForJava ---
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO] 
[INFO] --- compiler:3.11.0:compile (default-compile) @ TestcontainersForJava ---
         
・・・

23:14:14.591 [main] DEBUG com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.classic.MainClientExec -- ex-00000065: connection can be kept alive for 3 MINUTES
23:14:14.591 [main] DEBUG com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.classic.InternalHttpClient -- ep-00000064: releasing valid endpoint
23:14:14.591 [main] DEBUG com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager -- ep-00000064: releasing endpoint
23:14:14.591 [main] DEBUG com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager -- ep-00000064: connection http-outgoing-2 can be kept alive for 3 MINUTES
23:14:14.591 [main] DEBUG com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager -- ep-00000064: connection released [route: {}->unix://localhost:2375][total available: 1; route allocated: 1 of 2147483647; total allocated: 1 of 2147483647]
23:14:14.591 [main] DEBUG org.testcontainers.utility.ResourceReaper -- Removed container and associated volume(s): mysql:8.0
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.59 s -- in org.example.CustomerServiceTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  11.695 s
[INFO] Finished at: 2024-10-20T23:14:14+09:00
[INFO] ------------------------------------------------------------------------

まとめ

MySQL データベースを使用した Java アプリケーションのテストに Testcontainers for Java ライブラリを使う方法を試してみました。
Testcontainers を使用した統合テストの書き方は、IDE から実行できるユニットテストと非常に似ていて、簡単にセットアップできました。MySQL だけでなく、Testcontainers は他の多くの一般的な SQL データベース、NoSQL データベース、メッセージングキューなどに対応した専用モジュールを提供しています。Testcontainers を活用すれば、コンテナ化されたあらゆる依存関係を手軽にテスト環境で実行できるので、非常に便利そうだなと思いました!

レスキューナウテックブログ

Discussion