Testcontainers + @ServiceConnection入門:Spring Bootで本物のDBを使ったテストを書く

はじめに

Spring Bootでアプリケーションを開発していると、DBを使ったテストをどう書くかで悩むことがあります。

よくある選択肢は、次のようなものです。

・H2などのインメモリDBを使う
・ローカルにMySQLやPostgreSQLを起動してテストする
・Docker ComposeでDBを起動してからテストする
・Testcontainersでテスト用DBを自動起動する

小さなアプリケーションであればH2でも十分なことがあります。
しかし、実務ではMySQLやPostgreSQL固有のSQL、型、制約、インデックス、文字コード、トランザクション挙動などに依存する場面もあります。

その場合、H2ではテストが通るのに本番DBでは動かない、という問題が起きることがあります。

そこで便利なのが Testcontainers です。Testcontainersは、JUnitテストで利用できる軽量な使い捨てコンテナを起動するためのJavaライブラリで、DBやメッセージブローカーなどDockerで動くサービスをテスト用に立ち上げられます。

さらにSpring Bootでは、@ServiceConnection を使うことで、Testcontainersで起動したDBの接続情報をSpring Boot側へ自動で渡せます。これにより、spring.datasource.url をテストごとに手動で差し替えるようなコードを減らせます。

この記事では、Spring Bootで Testcontainers + @ServiceConnection を使い、本物のMySQLコンテナを使ったテストを書く方法を解説します。

この記事でわかること

・Testcontainersとは何か
・@ServiceConnectionで何が楽になるのか
・build.gradleの設定
・MySQLContainerを使ったテストの書き方
・DynamicPropertySourceとの違い
・Repositoryテストの実装例
・よくあるエラーと対処
・移行時のチェックポイント

この記事では Gradle + JUnit 5 + Spring Boot + MySQL を前提にします。

Testcontainersとは?

Testcontainersは、テスト実行時にDockerコンテナを起動し、テスト終了後に破棄するためのライブラリです。

例えば、MySQLを使うアプリケーションであれば、テスト時だけMySQLコンテナを立ち上げて、そのDBに対してRepositoryやServiceのテストを実行できます。

イメージは次の通りです。

JUnit Test
  ↓
Testcontainers
  ↓
MySQL Container
  ↓
Spring Boot ApplicationContext

ローカルPCにMySQLを直接インストールしなくても、Dockerさえ使えればテスト用DBを用意できます。

そのため、チーム開発やCI環境でも「誰の環境でも同じDBでテストする」状態を作りやすくなります。

@ServiceConnectionとは?

@ServiceConnection は、Spring BootがTestcontainersとの連携を簡単にするために用意しているアノテーションです。

従来、Testcontainersで起動したDBをSpring Bootの DataSource に接続するには、次のような設定を書くことが多くありました。

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

このコードは動きますが、毎回書くのは少し面倒です。

@ServiceConnection を使うと、コンテナから接続情報を自動的に作成し、Spring Bootの自動構成に渡してくれます。Spring Bootの公式ドキュメントでも、Testcontainers利用時にコンテナへ @ServiceConnection を付けると、実行中コンテナへの接続情報を自動作成できると説明されています。

つまり、以下のように書けます。

@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

これだけで、Spring BootがMySQLコンテナのJDBC URL、ユーザー名、パスワードを認識し、テスト用の DataSource に接続してくれます。

まず結論:基本形はこれ

最小構成は次のようになります。

@SpringBootTest
@Testcontainers
class SampleApplicationTests {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Test
    void contextLoads() {
    }
}

必要なポイントは3つです。

・@Testcontainers をテストクラスに付ける
・@Container をコンテナフィールドに付ける
・@ServiceConnection を付けてSpring Bootに接続情報を渡す

以前のように spring.datasource.url を手で差し替えなくてよいのが大きなメリットです。

build.gradleの設定

Gradleプロジェクトでは、以下のように依存関係を追加します。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.mysql:mysql-connector-j")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.boot:spring-boot-testcontainers")
    testImplementation("org.testcontainers:junit-jupiter")
    testImplementation("org.testcontainers:mysql")
}

ポイントは spring-boot-testcontainers を追加することです。

@ServiceConnection は Spring Boot の Testcontainers サポートに含まれる機能なので、Spring Boot側の連携用依存関係が必要です。Spring Bootの公式ドキュメントでも、サービス接続を利用するためのTestcontainers連携が説明されています。

また、MySQLを使う場合は次の2つも必要です。

・MySQL JDBC Driver
・Testcontainers MySQL module

PostgreSQLを使う場合は、MySQL部分をPostgreSQL向けに置き換えます。

runtimeOnly("org.postgresql:postgresql")
testImplementation("org.testcontainers:postgresql")

application.ymlはどうする?

Testcontainers + @ServiceConnection を使う場合、テスト用のDB接続情報を application.yml に固定で書く必要はありません。

通常のアプリケーション用には、例えば以下のように書いておきます。

spring:
  application:
    name: testcontainers-sample

  datasource:
    url: jdbc:mysql://localhost:3306/app_db
    username: app_user
    password: app_password

  jpa:
    hibernate:
      ddl-auto: validate

一方で、テスト実行時は @ServiceConnection がMySQLコンテナの接続情報を提供するため、テスト用に spring.datasource.url を上書きするコードを書かなくても動かせます。

テスト専用の設定を追加したい場合は、src/test/resources/application.yml を作って、JPAやログなどのテスト用設定だけを書くのがおすすめです。

spring:
  jpa:
    hibernate:
      ddl-auto: create-drop

logging:
  level:
    org.hibernate.SQL: debug

DB接続先はTestcontainers側に任せ、テストで必要な振る舞いだけ src/test/resources/application.yml に寄せると見通しが良くなります。

Repositoryテストのサンプル

ここからは、実際にEntityとRepositoryを作って、MySQLコンテナに対してテストしてみます。

Entity

package com.example.demo.user;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

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

    protected UserEntity() {
    }

    public UserEntity(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

Repository

package com.example.demo.user;

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

import java.util.Optional;

public interface UserRepository extends JpaRepository<UserEntity, Long> {

    Optional<UserEntity> findByEmail(String email);
}

Testcontainersを使ったテスト

package com.example.demo.user;

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.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Testcontainers
class UserRepositoryTest {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Autowired
    UserRepository userRepository;

    @Test
    void メールアドレスでユーザーを検索できる() {
        UserEntity savedUser = userRepository.save(
                new UserEntity("Taro", "taro@example.com")
        );

        Optional<UserEntity> result = userRepository.findByEmail("taro@example.com");

        assertThat(result).isPresent();
        assertThat(result.get().getId()).isEqualTo(savedUser.getId());
        assertThat(result.get().getName()).isEqualTo("Taro");
    }
}

このテストでは、実際のMySQLコンテナに対して insertselect が実行されます。

H2ではなくMySQLでテストするため、MySQL固有の挙動に近い状態でRepositoryを検証できます。

@DataJpaTestでも使える?

Repositoryだけをテストしたい場合は、@SpringBootTest ではなく @DataJpaTest を使うこともあります。

package com.example.demo.user;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@Testcontainers
class UserRepositoryDataJpaTest {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Autowired
    UserRepository userRepository;

    @Test
    void 保存できる() {
        UserEntity user = userRepository.save(
                new UserEntity("Hanako", "hanako@example.com")
        );

        assertThat(user.getId()).isNotNull();
    }
}

ただし、@DataJpaTest はテストスライスなので、読み込まれるBeanが限定されます。
ServiceやControllerまで含めた結合テストをしたい場合は、@SpringBootTest を使うほうが自然です。

DynamicPropertySourceとの違い

@ServiceConnection 登場前は、@DynamicPropertySource を使って接続情報を渡す書き方が一般的でした。

@SpringBootTest
@Testcontainers
class OldStyleTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

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

    @Test
    void contextLoads() {
    }
}

この書き方でも問題はありません。

ただ、Spring Boot 3.1以降のプロジェクトであれば、基本的には @ServiceConnection を使うほうがシンプルです。Spring Boot 3.1でTestcontainersサポートが改善され、@ServiceConnection によってコンテナから接続情報を自動で作成できるようになりました。

新しい書き方は以下です。

@SpringBootTest
@Testcontainers
class NewStyleTest {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Test
    void contextLoads() {
    }
}

差分はかなり小さく見えますが、テストが増えてくると効いてきます。

・接続情報登録のボイラープレートが減る
・DBごとのプロパティ名を意識しなくてよい
・Spring Bootの自動構成に任せやすい
・テストコードの意図が読みやすい

複数コンテナを使う場合

実務では、DBだけでなくRedisやKafkaなども使うことがあります。

Spring BootのTestcontainersサポートでは、複数のサービス接続にも対応しています。Spring Bootの公式ドキュメントでは、spring-boot-testcontainers に複数のサービス接続ファクトリが用意されていると説明されています。

例として、MySQLとRedisを起動する場合は次のように書けます。

@SpringBootTest
@Testcontainers
class MultipleContainersTest {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Container
    @ServiceConnection
    static GenericContainer<?> redis = new GenericContainer<>("redis:7")
            .withExposedPorts(6379);

    @Test
    void contextLoads() {
    }
}

ただし、サービスによっては専用Containerクラスや接続名の指定が必要になる場合があります。
まずは公式ドキュメントで対象サービスが @ServiceConnection に対応しているか確認するのが安全です。

Flyway / Liquibaseと組み合わせる

実務では、DBスキーマ管理にFlywayやLiquibaseを使っていることも多いです。

TestcontainersでMySQLを起動し、Spring Bootアプリケーションコンテキストを起動すると、通常のアプリ起動時と同じようにFlywayやLiquibaseのマイグレーションを流せます。

例えば、src/main/resources/db/migration/V1__create_users.sql に以下を置きます。

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE
);

テスト実行時にMySQLコンテナが起動し、その上にマイグレーションが適用され、その状態でRepositoryテストが実行されます。

これはかなり実務に近いテストです。

1. MySQLコンテナ起動
2. Spring Boot起動
3. Flyway / Liquibase 実行
4. Repository / Service のテスト実行
5. テスト終了後にコンテナ破棄

H2でテーブルを自動生成するより、本番に近いスキーマで検証しやすくなります。

テストデータはどう入れる?

テストデータの入れ方はいくつかあります。

・Repositoryで保存する
・JdbcTemplateでinsertする
・@Sqlを使う
・Flywayのテスト用マイグレーションを使う
・コンテナ起動時に初期化SQLを流す

最初はRepositoryやJdbcTemplateで明示的に入れるのがおすすめです。

@Test
void 検索できる() {
    userRepository.save(new UserEntity("Taro", "taro@example.com"));

    Optional<UserEntity> result = userRepository.findByEmail("taro@example.com");

    assertThat(result).isPresent();
}

テストメソッド内でデータの準備が見えるので、何を検証しているのかがわかりやすいです。

大量の初期データが必要な場合は、@Sql や初期化SQLを検討するとよいです。

テスト速度は遅くならない?

Testcontainersは実際にDockerコンテナを起動するため、H2よりは起動に時間がかかります。

そのため、すべてのテストをTestcontainersに置き換える必要はありません。

おすすめは役割分担です。

・純粋なロジック → 単体テスト
・Repository / DB制約 / SQL → Testcontainers
・Controllerの軽い確認 → MockMvc
・アプリ全体の起動確認 → @SpringBootTest + Testcontainers

Testcontainersは「DBや外部サービスとの結合部分を本物に近い環境で確認する」ために使うと効果が高いです。


開発時にも使える

Testcontainersはテストだけでなく、開発時サービスとして使うこともできます。

Spring Bootの公式ドキュメントでは、Testcontainersを結合テストだけでなく開発時にも利用できると説明されています。

例えば、開発用のTestcontainers設定を別クラスに切り出して、IDEから起動する方法があります。

package com.example.demo;

import org.springframework.boot.SpringApplication;
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)
public class TestcontainersConfiguration {

    @Bean
    @ServiceConnection
    MySQLContainer<?> mysqlContainer() {
        return new MySQLContainer<>("mysql:8.4");
    }
}
package com.example.demo;

import org.springframework.boot.SpringApplication;

public class TestDemoApplication {

    public static void main(String[] args) {
        SpringApplication
                .from(DemoApplication::main)
                .with(TestcontainersConfiguration.class)
                .run(args);
    }
}

この起動クラスをIDEから実行すると、開発用DBコンテナを起動した状態でアプリを動かせます。

ただし、普段のローカル開発環境を安定させたい場合は、Spring BootのDocker Compose連携を使う方法もあります。
テスト中心ならTestcontainers、普段のローカル開発環境ならDocker Compose連携、と整理すると選びやすいです。

よくあるエラーと対処

1. Dockerが起動していない

エラー例です。

Could not find a valid Docker environment

対処:

・Docker Desktopを起動する
・docker --version が実行できるか確認する
・docker ps が実行できるか確認する
・Windowsの場合、WSL2連携が有効か確認する

TestcontainersはDockerコンテナを利用するため、Docker環境が必要です。Testcontainers公式ドキュメントでも、Dockerで動くサービスをテスト用コンテナとして利用できることが説明されています。

2. MySQLの起動に時間がかかる

エラー例です。

Container startup failed
Communications link failure

対処:

・PCのDockerリソースを確認する
・MySQLイメージを事前にpullしておく
・タイムアウトが短すぎないか確認する
・不要なコンテナを停止する

初回はDockerイメージのダウンロードが入るため、時間がかかります。
2回目以降はイメージがキャッシュされるため、少し速くなります。

3. テストごとにデータが残る

同じコンテナや同じApplicationContextを使い回すと、テストデータが残るように見えることがあります。

対処:

・各テストでデータを明示的に削除する
・@Transactionalでロールバックする
・@Sqlで初期化する
・テストごとに必要なデータだけ作る

Repositoryテストでは、テストメソッドごとに必要なデータを作り、他のテストに依存しない形にするのが基本です。

4. application.ymlのDB接続先につながってしまう

@ServiceConnection を付け忘れている可能性があります。

@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

また、テスト用のプロファイルや src/test/resources/application.yml に固定の spring.datasource.url を書いている場合も確認しましょう。

5. CIでテストが失敗する

CI環境でDockerが使えない場合、Testcontainersは動きません。

対処:

・CIランナーでDockerが使えるか確認する
・Docker-in-Dockerの設定を確認する
・CIの権限やソケットマウントを確認する
・重い結合テストだけ別ジョブに分ける

GitLab CIやGitHub Actionsで使う場合は、Dockerが利用できる実行環境を選ぶ必要があります。

移行チェックリスト

H2やローカルDB前提のテストから、Testcontainers + @ServiceConnection へ移行するときは、以下を確認しましょう。

□ Dockerがローカル環境で使えるか
□ build.gradleに spring-boot-testcontainers を追加したか
□ testImplementationに org.testcontainers:junit-jupiter を追加したか
□ DBに応じたTestcontainersモジュールを追加したか
□ MySQL / PostgreSQL JDBC Driverを追加したか
□ テストクラスに @Testcontainers を付けたか
□ コンテナに @Container を付けたか
□ コンテナに @ServiceConnection を付けたか
□ DynamicPropertySourceを削除できるか確認したか
□ src/test/resources/application.ymlに不要なDB接続設定が残っていないか
□ Flyway / Liquibase がテスト時にも動くか
□ CI環境でDockerが使えるか

一気に全部置き換えるより、まずはRepositoryテストを1つだけTestcontainers化して、動作確認するのがおすすめです。

まとめ

Testcontainers + @ServiceConnection を使うと、Spring Bootで本物のDBを使ったテストをかなり簡単に書けます。

重要なポイントは以下です。

・Testcontainersはテスト時にDockerコンテナを起動するライブラリ
・H2ではなくMySQLやPostgreSQLでテストできる
・@ServiceConnectionを使うとDB接続情報を自動でSpring Bootに渡せる
・DynamicPropertySourceのような手動設定を減らせる
・RepositoryやServiceの結合テストに向いている
・CIで使う場合はDocker環境が必要

すべてのテストをTestcontainersにする必要はありません。
純粋なロジックは単体テスト、DBに依存する処理はTestcontainers、というように使い分けるのが現実的です。

特に、MySQLやPostgreSQL固有の挙動に依存するアプリでは、Testcontainersを導入する価値が大きいです。
Spring Bootの @ServiceConnection を使えば接続設定の手間もかなり減るため、これからSpring Bootで結合テストを整備するなら、ぜひ押さえておきたい機能です。

是非フォローしてください

最新の情報をお伝えします

類似投稿