Testcontainersとは?Dockerで作る使い捨てテスト環境と2.0系の移行点

テスト用のデータベースを共有サーバーに1つ置くと、誰かのテストが残したデータで他のテストが落ちます。開発者ごとに手で用意すれば、今度はバージョンや文字コードの差が結果の差になります。Testcontainersは、テストの実行中だけ本物のミドルウェアをDockerコンテナとして起動し、終了時に破棄するライブラリです。Javaを軸に、仕組み・最小構成・2025年10月の2.0系の破壊的変更・CI構成を、公式ドキュメントと公開アーティファクトの実測に基づいて整理します。

まとめ

Testcontainersは公式サイトの言葉で「データベース、メッセージブローカー、Webブラウザなど、Dockerコンテナで動かせるものの使い捨てインスタンスを提供するライブラリ」です。必要なのはDockerが動く環境だけで、Java・Go・.NET・Node.js・Pythonを含む13の実装が公式に並びます。日本語では「テストコンテナ」とも呼ばれます。起動したコンテナはJUnitの拡張が停止させ、取りこぼしはRyukという相棒コンテナが回収するため、テストコードでstop()を書く必要はありません。

版の扱いには注意が要ります。Java版は2.0.0(2025年10月14日公開)でモジュール名すべてにtestcontainers-接頭辞が付き、コンテナクラスがorg.testcontainers.<モジュール名>へ移動しました。1.x時代の記事どおりにorg.testcontainers:mysqlと書くと、2.0系では依存が解決できません。最新は2.0.5(2026年4月20日公開)、1.x系は1.21.4(2025年12月15日公開)が最終です(日付は公式リリースのUTC基準)。テストが遅い場合の第一手はシングルトンコンテナで、withReuse(true)による再利用は公式が実験的機能と明記しておりCIには向きません。以下、根拠と書き方を見ていきます。

使い捨てコンテナでテストする仕組み

テスト実行中だけミドルウェアを起動する流れ

テストコードがコンテナの定義を持ち、テスト開始時にイメージを取得して起動し、終了時に破棄します。ポート番号は固定せずホスト側の空きポートへ動的に割り当てられ、接続先はgetMappedPort()getJdbcUrl()で受け取ります。同じテストを並列で走らせてもポートが衝突しないのはこの設計によるものです。

起動完了の判定も自前では書きません。データベース用のクラスは接続が確立できるまで、Webサーバー用のクラスはWait.forHttp()が指定するレスポンスが返るまで待ちます。事前起動方式でつまずきやすい「コンテナは立ったがまだ初期化中で接続に失敗する」問題がここで消えます。

Ryukによる後始末とコンテナが残る条件

Testcontainersはテスト対象のコンテナとは別に、Ryuk(resource reaper)というコンテナを起動します。公式ドキュメントはその役割を「コンテナの削除と、JVM終了時における停止済みコンテナの自動クリーンアップに責任を持つ」と説明しています。テストが例外で中断してもコンテナが残らないのは、この仕組みが働くためです。

コンテナが残るのはRyukを止めた場合です。Podmanをrootlessモードで使う環境ではTESTCONTAINERS_RYUK_DISABLED=trueを指定しますが、公式はこの節に「Testcontainersは引き続きJVMのシャットダウン時にクリーンアップを行う。ただしkill -9でJVMプロセスを終了した場合を除く」と注記しています。無効化したうえで強制終了すると、後始末の経路が両方なくなります。rootfulモードで要求されていたTESTCONTAINERS_RYUK_PRIVILEGED=trueは1.19.0以降は不要です。

モックやDocker Composeによる事前起動との違い

モックは自分の想定を検証する道具であり、SQLの方言差、一意制約違反時のエラーコード、トランザクション分離レベルの挙動といった実装依存の部分は検出できません。ここを本物で確かめたい層にTestcontainersが効きます。Docker Composeで事前にコンテナを立てる方式も本物を使う点は同じでした。違いはライフサイクルの所在で、事前起動方式では起動順の制御・後始末・ポート衝突の回避が自分の担当になります。Dockerの隔離の原理はDockerの仕組みを原理から理解する|コンテナ隔離・アーキテクチャ・VMとの違いで解説しています。

JavaでのTestcontainers最小構成

@Testcontainersと@Containerの役割分担

JUnit 5と組み合わせる場合、必要なアノテーションは2つです。2.0.5系での書き方は次のようになります。

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

import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.mysql.MySQLContainer;

@Testcontainers
class OrderRepositoryTest {

    @Container
    static MySQLContainer mysql = new MySQLContainer("mysql:8.4")
            .withDatabaseName("testdb")
            .withUsername("app")
            .withPassword("app-secret");

    @Test
    void 割り当て済みポートを含むJDBC_URLが得られる() {
        assertTrue(mysql.getJdbcUrl().startsWith("jdbc:mysql://"));
    }
}

@TestcontainersはJUnit 5の拡張を有効にするスイッチで、@Containerを付けたフィールドだけが起動・停止の管理対象になります。片方だけではコンテナが起動しません。この2つは2.0系でもorg.testcontainers.junit.jupiterパッケージのままです。JUnit 5の書き方はJUnitとは?Javaの単体テストフレームワークの基本と書き方【2026年版】で整理しています。

イメージタグをmysql:latestにするのは避けてください。コードを変えていないのに別のメジャーバージョンが降ってきて、ある日突然テストが落ちます。mysql:8.4のように固定します。

staticフィールドとインスタンスフィールドで変わる寿命

staticを付けたフィールドのコンテナはテストクラスごとに1回だけ起動し、全テストメソッドで共有されます。付けない場合はテストメソッドごとに起動と破棄が走りました。データベースのように起動が数秒かかるものはstaticが基本で、メソッドごとの独立性はトランザクションのロールバックや初期化SQLの再実行で担保してください。

停止処理は書きません。@AfterEachmysql.stop()を呼ぶコードを見かけますが、@Containerを使っている限りJUnit拡張が停止させ、取りこぼしはRyukが回収します。

withInitScriptによる初期データ投入とマイグレーションの二重管理

テーブル定義や初期レコードは、クラスパス上のSQLファイルを起動直後に流す方法が簡潔です。

@Container
static MySQLContainer mysql = new MySQLContainer("mysql:8.4")
        .withDatabaseName("testdb")
        .withInitScript("db/schema.sql");

受け取るのはクラスパス上のパスで、src/test/resources/db/schema.sqlのような配置になります。FlywayやLiquibaseを使っているなら、初期化SQLを別に持たずアプリ側のマイグレーションをテスト起動時に走らせてください。スキーマ定義が2箇所に分かれると、片方だけ更新されて落ちます。

Spring Bootでの接続設定の委譲先

Spring Bootのテストでは、動的に決まるポートをspring.datasource.urlへ手で渡す作業自体が不要です。Spring Boot 3.1で追加された@ServiceConnectionをコンテナのフィールドに付けると、接続情報がConnectionDetailsとして自動登録されます。対応ミドルウェアの一覧と未対応時に出る例外は@ServiceConnectionとは|Spring BootのTestcontainers接続設定を自動化する方法にまとめています。

Testcontainers 2.0系で変わった依存とパッケージ

モジュール名の接頭辞とHTTP 404の関係

2.0.0のリリースノートが挙げる主な変更は3点です。JUnit 4サポートの削除、全モジュールへのtestcontainers-接頭辞の付与、コンテナクラスのorg.testcontainers.<モジュール名>パッケージへの移動。原文は「org.testcontainers:mysql is now org.testcontainers:testcontainers-mysql」と具体例まで示しています。

この改名は互換性のない変更です。org.testcontainers:mysqlは1.21.4が最終版で、Maven Centralからmysql-2.0.5.pomを取得しようとするとHTTP 404が返ります。「バージョンを2.0.5に上げただけでビルドが該当バージョンなしで落ちる」場合、原因はほぼこれです。Gradleでは次のように書き換えます。

// Testcontainers 1.21.4 まで
testImplementation "org.testcontainers:junit-jupiter:1.21.4"
testImplementation "org.testcontainers:mysql:1.21.4"

// Testcontainers 2.0.5
testImplementation "org.testcontainers:testcontainers-junit-jupiter:2.0.5"
testImplementation "org.testcontainers:testcontainers-mysql:2.0.5"

グループIDのorg.testcontainersは変わりません。BOMでバージョンを一元管理している場合も、アーティファクトIDの書き換えは個別に必要です。

コンテナクラスの移動とジェネリクスの除去

クラスの移動側は互換性が残されています。testcontainers-mysql:2.0.5のjarには新旧2つのMySQLContainerが同梱され、旧org.testcontainers.containers.MySQLContainerには@Deprecatedと「use org.testcontainers.mysql.MySQLContainer instead」というjavadocが付いていました。importを直さなくても当面は動きます。移動と削除は別物です。

見落としやすいのは新クラスでジェネリクスが外れている点です。旧クラスは自己参照型パラメータを持つMySQLContainer<SELF extends MySQLContainer<SELF>>でしたが、新クラスはpublic class MySQLContainer extends JdbcDatabaseContainer<MySQLContainer>という宣言です。フィールド定義をMySQLContainer<?>からMySQLContainerへ直す必要があり、この差はリリースノートに載っていません。Spring Boot側の書き換えは@ServiceConnectionとは|Spring BootのTestcontainers接続設定を自動化する方法にバージョン別の記述例があります。

1.x系から上げるときの確認順序

一度に全部を書き換えると、どの変更で落ちたのか分からなくなります。次の順で進めるとビルドエラーの原因が切り分けやすくなります。

  • JUnit 4のテストが残っていないか確認する。残っていれば2.0へ上げる前にJUnit 5へ移す
  • 依存のアーティファクトIDへtestcontainers-接頭辞を付ける
  • MySQLContainer<?>のようなワイルドカード付きの型宣言からジェネリクスを外す
  • importの非推奨警告を新パッケージへ寄せる

逆に数えなくてよい作業もあります。KafkaのConfluentKafkaContainerへの分離を2.0の変更として挙げる解説がありますが、このクラスは1.20.2のjarにすでに入っており、2.0固有の作業ではありません。

先頭のJUnit 4の棚卸しだけは2.0へ上げる前に済ませてください。ここが残っていると依存の書き換えとテストフレームワークの移行が同時に走り、切り分けが難しくなります。当面1.x系で止める判断も現実的で、その場合の最終版は1.21.4です。

対応言語ごとの実装と現行バージョン

公式サイトが挙げる実装はJava、Go、.NET、Node.js、Python、Rust、Ruby、PHP、Haskell、Clojure、Elixir、Scala、Nativeの13種類です。主要な実装の現行版は2026年8月5日時点で次のとおりです。

言語 パッケージ 現行版
Java org.testcontainers:testcontainers 2.0.5
Go testcontainers-go v0.43.0
Node.js testcontainers(npm) 12.1.0
Python testcontainers(PyPI) 4.15.0
.NET Testcontainers(NuGet) 4.13.0
Rust testcontainers(crates.io) 0.27.3
PHP testcontainers/testcontainers 1.0.10

実装ごとにバージョン体系は独立しており、JavaのモジュールIDが2.0で変わってもNode.js版やPython版のimportは影響を受けません。TypeScriptなら型定義を同梱するnpmパッケージをそのまま使えます。

import { GenericContainer } from "testcontainers";

const redis = await new GenericContainer("redis:7.4")
  .withExposedPorts(6379)
  .start();

const port = redis.getMappedPort(6379);

Python版はパッケージ側がPython 3.10以上を要求します。言語ごとに用意されたコンテナモジュールの数には差があるため、目的のミドルウェア用クラスの有無は各言語のドキュメントで確認してください。

Dockerランタイムの選択肢と設定の読み込み順

Docker Desktop以外の環境で動かす場合の指定

公式ドキュメントが挙げるのは、Docker Desktop(MacおよびWindows)、Linux上のDocker、Testcontainers Cloud、Colima、Podman、Rancher Desktopです。ただし同格ではありません。公式は代替ランタイムを「開発の主要ワークフローでは能動的にテストされていないため、全機能が使えるとは限らない」と断っています。差し替えは可能ですが、機能差が出る前提で選んでください。

接続先は環境変数で指定します。デーモンのエンドポイントがDOCKER_HOST、TLS関連がDOCKER_TLS_VERIFYDOCKER_CERT_PATH、ソケット位置の上書きがTESTCONTAINERS_DOCKER_SOCKET_OVERRIDE、コンテナから見たホスト名の上書きがTESTCONTAINERS_HOST_OVERRIDEです。

.testcontainers.propertiesとクラスパス設定の優先順位

設定はホームディレクトリの.testcontainers.propertiesとクラスパス上のtestcontainers.propertiesの2箇所に書けます。優先順位は環境変数、ホームの設定ファイル、クラスパスの順です。環境変数名はプロパティ名を大文字とアンダースコアに直してTESTCONTAINERS_を付けた形で、checks.disableならTESTCONTAINERS_CHECKS_DISABLEになります。

// ~/.testcontainers.properties
docker.host=unix:///Users/myuser/.colima/default/docker.sock
checks.disable=true
pull.timeout=180

他に指定できるのはクライアント選択戦略(docker.client.strategy)やRyukのイメージ(ryuk.container.image)などです。CIで効かせたい設定は環境変数、開発者ごとに違う設定はホームの設定ファイルへ置くと、リポジトリに個人環境の事情が混ざりません。

CI環境でTestcontainersを動かす構成

ソケットマウントによる基本形

Dockerを使ったテスト自動化をCIへ載せるとき、最初に決めるのがデーモンの与え方です。公式が主に推奨するのは、CIジョブのコンテナからホストのDockerデーモンを使い、テスト対象コンテナを兄弟として起動する構成で、ドキュメントではDocker wormholeパターンと呼ばれます。ソケットのマウントに加えてもう1つ条件があります。

その条件が、ソースコードのマウント先パスを揃えることです。公式は「ローカルのソースコードディレクトリは、Testcontainersが動くコンテナ内の同じパスにボリュームマウントされていなければならない」と明記しています。ファイルをコンテナへコピーする機能がホスト側のパスを基準に動くためです。

docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$PWD":"$PWD" \
  -w "$PWD" \
  eclipse-temurin:21 ./gradlew test

ホスト側の作業ディレクトリを同じパスへマウントし、そこを作業ディレクトリに指定するのがポイントです。GitHub Actionsのubuntuランナーのようにジョブがホスト上で直接動く環境なら、この配慮自体が不要です。ワークフローへの組み込みはGitHub Actionsでビルド・自動テストを設定する方法|CI/CDワークフローの作り方で扱っています。

Docker in Dockerを選ぶ条件

公式はDocker in Dockerについて「一般に最後の手段と見なされるが、一部のCI環境では必要になる」と書いています。ソケットを共有できない制約のあるCIサービスで初めて選ぶ構成で、イメージのキャッシュがジョブごとに失われやすくビルド時間でも不利です。Docker Desktop経由でCIコンテナを動かす場合はTESTCONTAINERS_HOST_OVERRIDEhost.docker.internalを設定します。

テスト起動時間を削る手段と副作用

シングルトンコンテナによる起動回数の削減

テストクラスが増えるほどクラスごとのコンテナ起動が積み上がります。抽象基底クラスの静的初期化ブロックで1回だけ起動し、各テストクラスがそれを継承すると、起動はJVMあたり1回で済みます。

import org.junit.jupiter.api.Test;
import org.testcontainers.mysql.MySQLContainer;

abstract class AbstractContainerBaseTest {

    static final MySQLContainer MY_SQL_CONTAINER;

    static {
        MY_SQL_CONTAINER = new MySQLContainer("mysql:8.4");
        MY_SQL_CONTAINER.start();
    }
}

class OrderQueryTest extends AbstractContainerBaseTest {

    @Test
    void 起動済みコンテナへ接続できる() {
        String url = MY_SQL_CONTAINER.getJdbcUrl();
    }
}

この形でも停止処理は書きません。公式は「テストスイートの終了時に、Testcontainersコアが起動したRyukコンテナがシングルトンコンテナの停止を引き受ける」と明記しています。代わりに要るのがテスト間のデータ分離で、各テストの冒頭でテーブルを初期化するかトランザクションをロールバックします。

コンテナ再利用が実験的機能である理由

テスト実行をまたいでコンテナを残すのが再利用機能です。コンテナ側にwithReuse(true)を指定したうえでstart()を手動で呼び、さらに環境変数TESTCONTAINERS_REUSE_ENABLE=trueかホームの設定ファイルのtestcontainers.reuse.enable=trueで有効化します。クラスパス上の設定ファイルでは有効になりません。リポジトリのコミットだけでCIの挙動が変わらないようにするための非対称な仕様です。

公式は再利用を「まだ実験的機能であり、挙動が変わる可能性がある」と位置づけ、「これらのコンテナはすべてのテストが終わっても停止しない」「CI環境には適さない」「リソースのクリーンアップやネットワークなど一部の機能が完全には動作しない」と続けています。使うなら開発者のローカル環境に閉じてください。CIで残ったコンテナは次のジョブへ古いデータを持ち込み、原因の分かりにくい失敗を生みます。JDBC URL方式で接続している場合は、URLへ?TC_REUSABLE=trueを付ける書き方が用意されています。

Testcontainersを採用しない方がよい場面

すべてのテストをTestcontainersに寄せる方針は失敗します。効くのは、外部ミドルウェアとの境界を検証する層に絞ったときです。次の条件に当てはまる場合は別の手段を先に検討してください。

  • Dockerデーモンに触れないCI環境。共有ランナーでソケットが非公開、特権コンテナが禁止といった制約があるなら、CIサービス側のサービスコンテナ機能かTestcontainers Cloudを検討する
  • 純粋なロジックの単体テスト。数ミリ秒で終わるテストに数秒のコンテナ起動を挟む理由はない
  • コンテナ版が存在しない外部サービス。決済代行のようなSaaSが相手なら、契約テストやスタブサーバーのほうが現実的
  • 画面を通した振る舞いの検証。ブラウザ操作を伴う検証はE2E(エンドツーエンド)テストのベストプラクティス|結合テストとの違いとツール選定で扱うレイヤの話になる

Docker Desktopのライセンス条件も導入前の確認事項です。公式ドキュメントは商用利用が無償になる範囲を「従業員250人未満かつ年間売上1,000万ドル未満の小規模事業者」と定めており(2026年8月時点)、超える組織はPro・Team・Businessのいずれかの契約が要ります。条件は改定されるため最新の内容は公式で確認し、満たせないならColimaやPodmanの検証を先に済ませてください。

よくある質問

testcontainers.propertiesはどこに置きますか

ホームディレクトリ直下の.testcontainers.properties(先頭がドット)と、クラスパス上のtestcontainers.propertiesの2箇所です。優先順位は環境変数、ホームの設定ファイル、クラスパスの順で、コンテナ再利用の有効化のようにホームの設定ファイルでしか受け付けない項目もあります。

テストが遅いときは何から確認しますか

切り分けの順序は、イメージ取得・起動回数・再利用の3段です。初回だけ極端に遅いならイメージのダウンロードが原因で、CIならレイヤキャッシュの有無を疑い、pull.timeoutの既定値で足りているかも見ます。毎回遅いならフィールドがstaticかを確認し、クラス数が多ければシングルトンへ寄せます。ここまでで足りないときに限り、ローカル環境でのコンテナ再利用を検討します。

テスト終了後にstop()を呼ぶ必要はありますか

@Containerを使っている場合は不要です。JUnit 5の拡張が停止させ、取りこぼしはRyukが回収します。シングルトンコンテナのように自分でstart()を呼ぶ構成でも、公式はテストスイート終了時にRyukが停止を引き受けると説明しています。例外はTESTCONTAINERS_RYUK_DISABLED=trueでRyukを止めている環境で、この場合はJVMのシャットダウン処理が唯一の後始末経路になります。

TypeScriptやPHPからも使えますか

使えます。Node.js版はnpmのtestcontainersパッケージ(現行版12.1.0)で、TypeScriptの型定義を同梱します。PHP版はtestcontainers/testcontainersとして公開され、現行版は1.0.10です。言語ごとに対応済みモジュールの数は異なるため、使いたいミドルウェア用のクラスがあるかは各言語のドキュメントで確認してください。

Testcontainers 2.0へ上げるとimport文は全部書き直しですか

書き直しが必須なのは依存のアーティファクトIDです。org.testcontainers:mysqlは2.0系が存在しないため、testcontainers-mysqlへ変えないとビルドが通りません。import文は旧パッケージのクラスがjarに残っており、@Deprecated警告が出るだけで動作します。ただし新クラスはジェネリクスが外れているため、MySQLContainer<?>という型宣言の書き換えは必要です。

関連記事

資料請求

RELATED POSTS 関連記事