Spring BootとTestcontainersで始めるMySQL統合テスト

Spring BootとTestcontainersで始めるMySQL統合テスト

RepositoryやMapperのテストでは、モックだけでは確認できないSQLの構文、値の保存、検索結果の対応があります。Testcontainersを使うと、テスト用のMySQLコンテナを起動し、Spring BootのDBアクセスをそのDBへ接続する構成を作れます。

この記事は、Spring BootでDBアクセスを実装していて、テスト用DBの準備から始めたい方向けです。依存関係、接続設定、スキーマの準備、保存・取得の検証、コンテナの管理までを扱います。ControllerやHTTP応答、業務固有のSQLの検証は対象に含めません。

この記事の例では、Java 17、Spring Boot 3.5、Testcontainers 1.x、Maven、JdbcTemplateを使います。

目次

MySQL統合テストの対象と準備する環境

Repository・DBアクセスで確認する範囲を決める

最初のテストでは、RepositoryからINSERTを実行し、同じ主キーでSELECTした値が期待どおりかを確認します。この範囲なら、Springが用意したDataSource、JDBCドライバ、SQL、MySQLへの接続を一つの流れで確認できます。

本記事のRepositoryは、JdbcTemplateを使う小さなクラスです。Spring Data JPAのRepositoryではありません。JPAやMyBatisでもテスト用DBを接続する考え方は共通ですが、EntityやMapperの読み込み、SQLの実行タイミングなどは、それぞれの構成に合わせます。

保存・取得ができても、すべての業務処理や本番運用を検証できたことにはなりません。まずDB接続と初期化を確認し、その後に対象のSQLやトランザクションに必要なケースを追加します。

Dockerと開発環境に合わせたMySQLイメージを用意する

Testcontainersを実行するプロセスから、対応するDocker環境へ接続できる必要があります。ローカルではDockerの起動状態やコンテナの実行方式、CIではランナーからDockerデーモンへ到達できる構成を確認します。

MySQLイメージは、開発環境のDB製品とバージョンを確認して選びます。再現性が必要なら、追跡可能な具体的なタグやダイジェストを使い、文字コード、照合順序、タイムゾーン、SQLモードも検証対象に合わせます。イメージだけを合わせても、これらの設定が自動的に一致するわけではありません。

MariaDBなど別のMySQL系DBを使っている場合は、その製品に対応するモジュールとイメージを選び直してください。マネージドDBを使う場合は、コンテナで確認する範囲と、実際の運用環境で確認する範囲を分けます。

Spring BootとTestcontainersの依存関係をそろえる

Spring Boot・TestcontainersのBOMに合わせて管理する

依存関係は、まずプロジェクトで使っているSpring Bootの親POMまたはBOMを基準にします。BOMは複数のライブラリのバージョンをまとめて管理するためのものです。Testcontainersの各モジュールへ個別に異なるバージョンを指定することは避けます。

以下の座標とimportは、Spring Boot 3.5の依存管理を利用し、Testcontainers 1.xを使う例として統一しています。パッチバージョンは固定していません。既存プロジェクトでTestcontainersのBOMを上書きしている場合は、実際に解決される系列を確認してください。別のメジャー系列へ変更する場合は、モジュール名とJavaのパッケージも含めて例全体を見直します。

親POMを使わずBOMだけを取り込む構成では、Javaのコンパイル設定やJUnit Jupiterを実行するMavenのテストプラグインも別途整えます。依存関係が解決できることと、テストが検出・実行されることは分けて確認しましょう。

MySQLモジュール・JDBCドライバ・JUnit連携を追加する

次は、Spring Bootの依存管理を設定済みのPOMへ追加する断片です。すでに同じ依存関係がある場合は重複させません。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-testcontainers</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mysql</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
依存関係この例での役割
spring-boot-starter-jdbcDataSourceとJdbcTemplateを使う構成
mysql-connector-jJavaからMySQLへ接続するJDBCドライバ
spring-boot-starter-testJUnit JupiterやSpringのテスト支援
spring-boot-testcontainers@ServiceConnectionなどのSpring Boot連携
org.testcontainers:mysqlMySQLコンテナの定義と起動支援
org.testcontainers:junit-jupiter@Testcontainersと@ContainerによるJUnit管理

MySQLモジュールとJDBCドライバは別に必要です。 また、JUnit側にコンテナを管理させる例ではTestcontainersのJUnit Jupiter連携モジュールを使います。上のドライバをruntimeにしたのは、アプリケーションもMySQLを使う想定のためです。テストだけで必要なプロジェクトなら、依存スコープをその用途に合わせてください。

@ServiceConnectionでMySQLの接続先を設定する

spring-boot-testcontainersとコンテナ定義を用意する

まず、テスト用コンテナを組み立てるクラスを用意します。以下のテスト用Javaファイルは、すべてsrc/test/java/com/example/dbtest/へ置く想定です。MySQLイメージは対象環境に合わせて指定できるよう、JVMのシステムプロパティtest.mysql.imageから受け取ります。

TestMysql.javaはコンテナを定義するだけで、このメソッド内では起動しません。withInitScriptで指定するSQLは後の節で用意します。

package com.example.dbtest;

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

final class TestMysql {
    private TestMysql() {
    }

    static MySQLContainer<?> create() {
        String image = System.getProperty("test.mysql.image");
        if (image == null || image.isBlank()) {
            throw new IllegalArgumentException(
                    "Set -Dtest.mysql.image to the selected MySQL image");
        }

        return new MySQLContainer<>(DockerImageName.parse(image))
                .withDatabaseName("repository_test")
                .withUsername("test_user")
                .withPassword("test_password")
                .withInitScript("db/test-schema.sql");
    }
}

既存DBの認証情報をテストコードへ書き込まないでください。また、この例は通常のMySQLイメージを前提とし、独自ミラーの互換性設定などは含めていません。

テスト用の接続詳細をSpring Bootへ渡す

JUnit管理では、テストクラスのstaticフィールドへ@Containerと@ServiceConnectionを付けます。次は後で掲載するテストクラス内のフィールドです。これだけを独立したJavaファイルにはしません。

@Container
@ServiceConnection
static MySQLContainer<?> mysql = TestMysql.create();

@ServiceConnectionは、MySQLContainerから取得できる接続詳細をSpring Bootの自動構成へ渡します。コンテナのホスト名や割り当てられたポートを利用できるため、テスト用のJDBC URLをlocalhost:3306へ固定する必要はありません。

この接続詳細を利用するDataSourceの自動構成では、対応する通常の接続プロパティより接続詳細が優先されます。ただし、独自に生成したDataSourceや複数DataSourceのすべてを自動的に置き換える仕組みではありません。本記事の例は、単一の自動構成DataSourceを前提とします。

接続先を渡す役割と、コンテナを起動・停止する役割は別です。 この例では接続詳細を@ServiceConnection、起動・停止をJUnitの@Testcontainersと@Containerが担当します。

@DynamicPropertySourceで接続プロパティを登録する代替方法

接続プロパティを明示的に登録する構成では、@DynamicPropertySourceを使えます。先ほどのフィールドから@ServiceConnectionを外し、次のメソッドを同じテストクラスへ追加します。@Testcontainersと@Containerによる起動管理は残します。

// テストクラスのimportへ追加する
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

// 以下はテストクラス内へ追加する
@DynamicPropertySource
static void datasourceProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", mysql::getJdbcUrl);
    registry.add("spring.datasource.username", mysql::getUsername);
    registry.add("spring.datasource.password", mysql::getPassword);
}

これは主手順に追加する設定ではなく、接続情報の渡し方を置き換える例です。同じDataSourceへ両方式を重ねると、どちらの設定を使っているか追いにくくなります。また、@DynamicPropertySource自体がコンテナを起動するわけではありません。

テストDBを初期化して保存・取得を検証する

スキーマと初期データの準備方法を決める

スキーマを作る手段は一つに決めます。すでにFlywayやLiquibaseで管理しているなら、テストでもそのマイグレーションを適用する構成が候補です。接続だけを確認するために別のDDLを用意すると、実際のスキーマとの差を見落とす可能性があります。

本記事では最小の接続例に絞り、Testcontainersの初期化スクリプトを使います。src/test/resources/db/test-schema.sqlへ次の内容を保存します。Flyway、Liquibase、Spring BootのSQL初期化とは併用しない前提です。

CREATE TABLE sample_notes (
    id BIGINT NOT NULL PRIMARY KEY,
    body VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

このスクリプトはコンテナの起動に伴う初期化で使われ、各テストメソッドの前に再実行されるものではありません。テーブル作成はここで行い、検証する行はテスト内で作ります。照合順序などの詳細設定は、この最小例では固定していません。

最小のRepositoryテストで保存した値を確認する

検証対象のNoteRepository.javaをsrc/main/java/com/example/dbtest/へ置きます。固定したSQLへ引数をバインドして保存し、IDに対応する本文を取得するだけのクラスです。

package com.example.dbtest;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class NoteRepository {
    private final JdbcTemplate jdbc;

    public NoteRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public int save(long id, String body) {
        return jdbc.update(
                "INSERT INTO sample_notes (id, body) VALUES (?, ?)",
                id, body);
    }

    public String findBody(long id) {
        return jdbc.queryForObject(
                "SELECT body FROM sample_notes WHERE id = ?",
                String.class, id);
    }
}

テスト用のDbTestConfig.javaでは、自動構成と検証対象のRepositoryを読み込みます。アプリケーション全体のコンポーネントスキャンを行わず、この例に必要な構成を明示します。前掲の依存関係だけを使う最小構成が前提です。

package com.example.dbtest;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@Import(NoteRepository.class)
class DbTestConfig {
}

NoteRepositoryTest.javaがテスト本体です。先ほどのTestMysql.javaとSQLファイルもそろえて配置します。

package com.example.dbtest;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

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

@Testcontainers
@SpringBootTest(
        classes = DbTestConfig.class,
        webEnvironment = SpringBootTest.WebEnvironment.NONE,
        properties = "spring.sql.init.mode=never")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@Transactional
class NoteRepositoryTest {
    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = TestMysql.create();

    @Autowired
    NoteRepository repository;

    @Test
    void savesAndReadsBody() {
        int inserted = repository.save(1L, "みかんのメモ");

        assertEquals(1, inserted);
        assertEquals("みかんのメモ", repository.findBody(1L));
    }
}

このテストでは、INSERTの更新件数が1件であることと、保存した日本語の値を同じIDで取得できることを確認します。テストはトランザクション内で読み戻すため、コミット後に別接続から見えることまでは確認しません。

実行時にはmvn -Dtest.mysql.image=mysql:YOUR_TAG testのYOUR_TAGを、対象環境に合わせて選定した実在するタグへ置き換えます。IDEから実行する場合も、テストJVMへ同じシステムプロパティを設定します。テストが検出され、実行件数が0になっていないことも確認してください。

既存プロジェクトでは、自動構成や設定ファイル、追加ライブラリの影響も受けます。特にFlywayやLiquibaseを導入済みなら、この例の初期化スクリプトをそのまま重ねず、スキーマ準備の担当を決め直します。

テスト間でデータが残らないようにする

上の例はSpringのテスト用トランザクションを使い、テスト終了時の既定のロールバックでINSERTした行を戻す構成です。InnoDBのテーブルに対して、同じトランザクションへ参加するJdbcTemplateを同期的に呼び出しています。

ただし、別スレッドの処理、別のトランザクションでコミットした変更、DDLなどまで一律に戻せるわけではありません。とくにMySQLのDDLやTRUNCATEを、通常の行更新と同じロールバック前提で扱わないようにします。

ロールバックで分離できないテストでは、専用DBに対する明示的なデータ削除や、テスト単位のDB・コンテナ分離を設計します。コンテナやApplicationContextの共有は、データの自動リセットを意味しません。

DBの準備ができたら、MyBatisを使うプロジェクトでは、検索条件と返却行の対応などをMapperのテストへ追加できます。動的SQLの検証観点は次の記事で補足しています。

コンテナとSpring TestContextのライフサイクルを合わせる

JUnit管理のstatic・非staticコンテナの停止タイミング

JUnit Jupiter連携では、@Containerを付けたフィールドがstaticかどうかによって管理単位が変わります。

JUnit管理の定義起動・停止の単位DBテストへの影響
staticフィールドテストクラス内で共有し、クラスのテスト終了後に停止するメソッド間では同じDBを使うため、データ分離が必要
非staticフィールド各テストの前に起動し、後に停止するSpringのコンテキストが持つ接続先との寿命のずれに注意が必要

非staticにすればSpring BootのDataSourceも毎回自動的に作り直される、という関係ではありません。DBコンテナを作り直してポートが変わっても、既存のDataSourceが以前の接続先を保持する可能性があります。導入例では、単一クラスのstaticコンテナを使っています。

コンテキストキャッシュとコンテナ停止のずれに注意する

Spring TestContextは、条件が一致するテストでApplicationContextを再利用します。一方、JUnit管理のstaticコンテナは、対象クラスの終了時に停止します。コンテキストが再利用されても、接続先のコンテナがすでに停止していればDBへ接続できません。

前掲の単一クラス例には@DirtiesContext(AFTER_CLASS)を付け、終了したクラスのコンテキストを後続テストが再利用しないようにしています。コンテキスト再構築の負担があるほか、JUnitとSpring双方の終了処理の順序は、この指定とは別に確認する必要があります。

共通の親クラスへstaticコンテナを置くだけでは、複数のテストクラス全体を通じてコンテナを維持できるとは限りません。コンテキストを再利用する構成へ広げる場合は、コンテナもSpring側に管理させる方法を検討します。

Spring Beanや@ImportTestcontainersで管理する構成

Spring管理へ切り替える場合は、コンテナをテスト用のBeanとして定義できます。MySqlTestConfig.javaをテストソースへ追加します。

package com.example.dbtest;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MySQLContainer;

@TestConfiguration(proxyBeanMethods = false)
class MySqlTestConfig {
    @Bean
    @ServiceConnection
    MySQLContainer<?> mysql() {
        return TestMysql.create();
    }
}

この構成を使うテストは次の形です。先ほどのJUnit管理例の代替として使い、@Testcontainers、テストクラス内の@Containerフィールド、@DirtiesContextを持ち込みません。

package com.example.dbtest;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.Transactional;

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

@SpringBootTest(
        classes = DbTestConfig.class,
        webEnvironment = SpringBootTest.WebEnvironment.NONE,
        properties = "spring.sql.init.mode=never")
@Import(MySqlTestConfig.class)
@Transactional
class NoteRepositorySpringManagedTest {
    @Autowired
    NoteRepository repository;

    @Test
    void savesAndReadsBody() {
        assertEquals(1, repository.save(1L, "みかんのメモ"));
        assertEquals("みかんのメモ", repository.findBody(1L));
    }
}

Spring BootがコンテナBeanの起動・停止をApplicationContextのライフサイクルに合わせて管理します。同じコンテキストを再利用するテストでは、そのコンテナも共有されます。ただし、設定の異なるコンテキストや別のテストJVMまで、一つのコンテナになるわけではありません。

もう一つの選択肢が、既存のコンテナ宣言をテスト構成へ取り込む@ImportTestcontainersです。Spring Boot 3.5ではorg.springframework.boot.testcontainers.context.ImportTestcontainersを使います。コンテナ宣言の再利用に向きますが、同じインスタンスをJUnitとSpringの両方で起動・停止しないよう、管理する側を統一してください。

Spring管理へ変更しても、共有DBの行が各テスト前に消えるわけではありません。コンテナの寿命と、ロールバック・データ削除などの分離方針は、それぞれ決める必要があります。

Docker・CIで実行するときの確認ポイント

Dockerへの接続とイメージ取得条件を確認する

ローカルで使う構成をCIへ移すときは、Dockerがあることだけでなく、テストプロセスから使えるかを確認します。次の項目を、採用するランナーの方式に合わせて整理してください。

  • テストプロセスからDockerデーモンへ接続でき、コンテナを作成・停止できるか。
  • 選択したMySQLイメージが、ランナーのCPUアーキテクチャとコンテナ実行方式に対応しているか。
  • レジストリの認証、取得回数の制限、プロキシ、ネットワーク制限に対応できるか。
  • DB起動に必要なメモリ・ディスク容量と、ジョブのタイムアウトに余裕があるか。
  • テストプロセスから、コンテナの公開された接続先へ到達できるか。
  • コンテナの後片付けに必要な仕組みが、ランナーの権限や制限の下で利用できるか。

特に、テスト自体をコンテナ内で動かす構成では、Dockerへ接続する経路とMySQLへ接続する経路を別々に確認します。ホストの固定ポートを前提にせず、Testcontainersが提供する接続情報を使うことが基本です。

JUnit連携の並列実行制限とDBの共有範囲を確認する

JUnit Jupiterの並列実行を有効にする場合は、利用するTestcontainersのバージョンにおける拡張の対応条件を確認してください。MySQLコンテナを共有する構成では、並列テストが同じDBやテーブルを同時に変更しないよう、テストデータを分離します。

Spring管理のコンテナへ切り替えても、並列テストのデータ競合が自動的に解消されるわけではありません。同じ主キーのINSERT、別テストのデータ削除、共有行の更新などが干渉し得ます。テストごとのDB分離などを設計するまでは、共有DBを使うケースを順番に実行する構成が分かりやすいでしょう。

JUnitのメソッド並列化に加えて、Mavenの複数JVM、複数モジュール、CIの複数ジョブも確認します。別JVMや別ジョブではコンテナが複数起動する可能性があり、DBの共有範囲とランナーの必要リソースが変わります。

起動・接続・初期化のどこで失敗したかを切り分ける

テストが失敗したら、assertionだけを見るのではなく、どの段階まで進んだかを確認します。次の表の順に確認対象を切り分けていきましょう。

段階主な確認対象
コンパイル・テスト検出BOMで解決された系列、モジュール座標とimport、JUnitの実行設定
コンテナ定義・Docker接続test.mysql.imageの指定、Dockerの起動状態、接続先と権限
イメージ取得・DB起動タグ、レジストリへの接続、CPUアーキテクチャ、DBログ、リソース
初期化スクリプトリソースの配置、DDLの構文、DBユーザーの権限
DataSource作成・接続JDBCドライバ、接続詳細の取り込み、独自DataSourceの有無、接続先の寿命
RepositoryのSQL実行テーブル・列名、型、制約、バインド値
期待値との比較保存・取得した値、更新件数、前のテストから残ったデータ

この例のwithInitScriptによる初期化は、SpringのDataSourceを利用する前に行われます。Flywayなどを採用した場合は流れが変わるため、例外の原因とその前後のログを合わせて追ってください。調査記録には接続パスワードなどの認証情報を含めないようにします。

実行結果を残す際は、Java、Spring Boot、解決されたTestcontainersとJDBCドライバ、MySQLイメージ、Docker環境、実行コマンド、期待値と結果を記録すると前提を追えます。所要時間や成功可否は、その環境で得られた記録に基づいて判断します。

接続とデータ分離を確認できた後は、実際のRepositoryが担う処理へ検証を広げます。更新競合を扱う場合の設計観点は、次の記事が補足になります。本記事の単一トランザクションの保存・取得テストだけでは、複数処理間の競合までは確認できません。

まとめ

Spring BootとTestcontainersによるMySQL統合テストは、依存関係をそろえ、テスト用DBの接続先と初期化方法を決めるところから始めます。最初はRepositoryの保存・取得に絞ると、どこまで接続できているかを確認しやすくなります。

  • Spring Bootの依存管理を基準に、MySQLモジュール、JDBCドライバ、必要なJUnit連携をそろえる。
  • 接続詳細は@ServiceConnectionを主手順とし、@DynamicPropertySourceは代替として使う。
  • スキーマ作成の担当を一つに決め、テストデータの準備と後片付けを別に設計する。
  • 単一クラスのJUnit管理と、コンテキスト再利用を考慮したSpring管理を使い分ける。
  • Docker・CIの接続条件と、並列実行時のDB共有範囲を確認する。

参考資料は、Spring Boot 3.5のTestcontainers連携、TestcontainersのMySQLモジュール、JUnit Jupiter連携です。利用中のSpring Boot・Testcontainers・Dockerの版と構成に合わせ、依存関係とテスト動作を確認してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次