テスト用のデータベースを共有サーバーに1つ置くと、誰かのテストが残したデータで別のテストが落ちます。開発者ごとに手で用意すれば、今度はバージョンや文字コードの違いが結果の違いになります。Testcontainersは、テストの実行中だけ本物のミドルウェアをDockerコンテナとして起動し、終了時に破棄するライブラリです。仕組み、Java・Python・Goの最小構成、2025年10月に出たJava版2.0系の破壊的変更、Docker Desktop以外の環境やCIで動かす設定を、2026年11月3日に手元で実行した結果と公式資料に基づいて整理します。
まとめ
Testcontainersは公式サイトの説明で「データベース、メッセージブローカー、Webブラウザなど、Dockerコンテナで動かせるものの使い捨てインスタンスを提供するオープンソースのライブラリ」です。必要なのはDockerが動く環境だけで、Java・Go・.NET・Node.js・Pythonを含む13の実装が公式に並びます。開発元のAtomicJarは2023年12月11日にDocker社が買収を発表しました。Javaでは@Containerを付けたコンテナをJUnit拡張が停止させ、取りこぼしはRyukという後始末用のコンテナが回収するため、stop()は書きません。PythonやGoでは、with文や後始末用の関数で終了処理を登録します。
版の扱いには注意が要ります。Java版は2.0.0(2025年10月14日公開)で全モジュールにtestcontainers-接頭辞が付き、org.testcontainers:mysqlのような1.x時代の依存は2.0系では解決できません。2026年11月3日時点の最新は2.0.5で、1.x系の最終は1.21.4です。Python版も4.15.0でモジュールがtestcontainers.community配下へ移りました。ColimaなどDocker Desktop以外で動かすときは、Java・Python・GoのいずれもTESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sockを指定しないとRyukの起動で失敗しました。テストが遅いときの第一手はコンテナをstaticにして共有することで、3つのテストメソッドで実測すると50.2秒が17.2秒になりました。
使い捨てコンテナでテストする仕組み
テスト実行中だけミドルウェアを起動する流れ
テストコードがコンテナの定義を持ち、テスト開始時にイメージを取得して起動し、終了時に破棄します。ポート番号は固定せず、ホスト側の空きポートへ動的に割り当てられます。接続先はgetHost()とgetMappedPort()、データベースならgetJdbcUrl()で受け取ります。手元の実行ではjdbc:mysql://localhost:32790/testdbのように毎回違うポートが返り、並列に走らせても衝突しません。
起動完了の判定も自分では書きません。Java版の既定の待機戦略はWait.forListeningPort()で、公式ドキュメントは「通常は最初にマッピングされたポートが待ち受けを始めるまで最大60秒待つ」と説明しています。データベース用のクラスは接続を確認するまで待つ独自の判定を持ち、WebサーバーならWait.forHttp()で特定のレスポンスを待てます。
Ryukによる後始末とコンテナが残る条件
Testcontainersはテスト対象とは別に、Ryuk(resource reaper)というコンテナを起動します。公式の設定ドキュメントはその役割を「コンテナの削除と、JVM終了時における停止済みコンテナの自動クリーンアップ」と説明しています。手元の実行でもRyukはtestcontainers/ryuk:0.14.0として約1.1秒で起動し、テスト終了後にはMySQLのコンテナが一覧から消えていました。
コンテナが残りうるのはRyukを止めた場合です。Podmanをrootlessモードで使う環境ではTESTCONTAINERS_RYUK_DISABLED=trueを指定しますが、公式はこの節に「kill -9でJVMを終了した場合を除き、JVMのシャットダウン時にクリーンアップは行われる」と注記しています。無効化したうえで強制終了すると、後始末の経路が両方なくなります。後述の再利用機能を有効にした場合は、コンテナが意図的に残されます。
モックやDocker Composeによる事前起動との違い
モックはSQLの方言差、一意制約違反時のエラーコード、トランザクション分離レベルの挙動といった実装に依存する部分を検出できません。ここを本物で確かめたい層にTestcontainersが効きます。Docker Composeで事前にコンテナを立てる方式も本物を使いますが、起動順の制御、後始末、ポート衝突の回避が自分の担当になります。コンテナ隔離の原理はDockerの仕組みを図解:containerd・runc・namespaces・ネットワーク・Composeの内部を実測で解説にまとめています。
JavaでのTestcontainers最小構成
@Testcontainersと@Containerの役割分担
JUnit 5と組み合わせる場合、必要なアノテーションは2つです。依存はorg.testcontainers:testcontainers-junit-jupiter:2.0.5とorg.testcontainers:testcontainers-mysql:2.0.5、それにJDBCドライバです。次のコードは、JUnit JupiterとMySQLのJDBCドライバを追加したMavenプロジェクトで、Testcontainers 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のBOMを6.1.3に差し替えても通りました。JUnit自体の書き方はJUnitとは?Javaの単体テストの書き方・実行結果の読み方とJUnit 6の変更点で扱っています。
staticフィールドとインスタンスフィールドで変わる起動回数
staticを付けたコンテナはテストクラスごとに1回だけ起動し、全テストメソッドで共有されます。付けない場合はメソッドごとに起動と破棄が走ります。計測では初期化SQLでテーブルとレコードを用意し、レコード数を確認する3メソッドを両方の書き方で実行しました。イメージ取得済みの状態での結果が次の表です。
| フィールド | 起動したコンテナ | クラスの実行時間 |
|---|---|---|
| static | 1個(3メソッドで同じID) | 17.2秒 |
| インスタンス | 3個(メソッドごとに別ID) | 50.2秒 |
MySQL 8.4の起動は1回あたり13.6〜16.3秒かかっており、その回数がそのまま差になりました。秒数はColima上のx86_64仮想マシンでの値で環境により変わりますが、メソッド数だけ起動が積み上がる関係は変わりません。データベースはstaticを基本にし、メソッドごとの独立性はテーブルの初期化やトランザクションのロールバックで確保してください。
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箇所に分かれると片方だけ更新されて落ちます。FlywayをSpring Bootのテストで動かす設定はFlywayとは?Spring Bootのマイグレーション設定・Callbacks・Spring Boot 4の変更点にあります。
Spring Bootの@ServiceConnectionによる接続設定の自動化
Spring Bootのテストでは、動的に決まるポートをspring.datasource.urlへ手で渡す作業自体が要りません。Spring Boot 3.1で追加された@ServiceConnectionをコンテナのフィールドに付けると、接続情報が自動で登録されます。対応するミドルウェアの一覧と@DynamicPropertySourceとの使い分けは@ServiceConnectionとは|Spring BootのTestcontainers接続を自動化する書き方と@DynamicPropertySourceとの違いで解説しています。
PythonとGoでのTestcontainers最小構成
Python 4.15.0とpytestフィクスチャによるPostgreSQLの起動
Python版はPyPIのtestcontainersパッケージで、4.15.0(2026年7月24日公開)はPython 3.10以上を要求します。PostgreSQLを使うならpip install "testcontainers[postgres]==4.15.0" "psycopg[binary]" pytestで入れます。次のコードを実行し、テストが通ることを確認しました。
import psycopg
import pytest
from testcontainers.community.postgres import PostgresContainer
@pytest.fixture(scope="module")
def pg():
with PostgresContainer("postgres:17-alpine", driver=None) as container:
yield container
def test_select(pg):
url = pg.get_connection_url()
with psycopg.connect(url) as conn:
assert conn.execute("SELECT 1").fetchone()[0] == 1
注意点は2つあります。1つ目はimportの場所です。4.14.2までの解説にあるfrom testcontainers.postgres import PostgresContainerも4.15.0で動きますが、実行すると「testcontainers.postgres is deprecated, use testcontainers.community.postgres instead」というDeprecationWarningが出ます。4.14.2の配布物にはcommunity配下のPostgreSQLモジュール自体が無く、4.15.0で入った変更です。新しく書くならtestcontainers.communityから読み込んでください。
2つ目は接続URLの形式です。get_connection_url()はSQLAlchemy向けのURLを返し、既定ではドライバ名が付いたpostgresql+psycopg2://の形になります。psycopg(バージョン3)へ直接渡すなら、上のようにdriver=Noneを指定するとpostgresql://test:test@localhost:32800/testのようなURLが返ります。scope="module"はJavaのstaticと同じく、ファイル内のテストで1つのコンテナを共有する指定です。
Go v0.44.0とpostgres.RunによるPostgreSQLの起動
Go版はgithub.com/testcontainers/testcontainers-goで、2026年11月3日時点の最新はv0.44.0です。モジュールごとのRun関数で起動する書き方はPostgreSQLモジュールではv0.32.0で入りました。次のコードはv0.44.0とpgx v5.12.0で実行し、通ることを確認しています。
package tcdemo
import (
"context"
"database/sql"
"testing"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
func TestSelect(t *testing.T) {
ctx := context.Background()
pg, err := postgres.Run(ctx, "postgres:17-alpine",
postgres.WithDatabase("testdb"),
postgres.WithUsername("app"),
postgres.WithPassword("app-secret"),
postgres.BasicWaitStrategies(),
)
testcontainers.CleanupContainer(t, pg)
if err != nil {
t.Fatal(err)
}
dsn, err := pg.ConnectionString(ctx, "sslmode=disable")
if err != nil {
t.Fatal(err)
}
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var n int
if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&n); err != nil || n != 1 {
t.Fatalf("n=%d err=%v", n, err)
}
}
testcontainers.CleanupContainerはエラー判定より前に呼びます。起動の途中で失敗してもコンテナが作られている場合があり、テスト終了時に確実に片付けるためです。BasicWaitStrategies()はPostgreSQLの起動ログとポートの待ち受けを待つ設定で、これを省くと初期化中の接続で失敗しやすくなります。パッケージ内の全テストで共有したいときは、TestMainで1回だけ起動する形に変えます。
対応言語ごとの実装と現行バージョン
公式サイトが挙げる実装はJava、Go、.NET、Node.js、Python、Rust、Ruby、PHP、Haskell、Clojure、Elixir、Scala、Nativeの13種類です。主要な実装の現行版を、2026年11月3日に各パッケージレジストリで確認しました。
| 言語 | パッケージ | 現行版 |
|---|---|---|
| Java | org.testcontainers:testcontainers | 2.0.5 |
| Go | testcontainers-go | v0.44.0 |
| Node.js | testcontainers(npm) | 12.2.1 |
| Python | testcontainers(PyPI) | 4.15.0 |
| .NET | Testcontainers(NuGet) | 4.16.0 |
| Rust | testcontainers(crates.io) | 0.28.0 |
| PHP | testcontainers/testcontainers | 1.2.0 |
バージョン体系は実装ごとに独立しています。JavaのモジュールIDが2.0で変わっても、Node.js版やGo版のimportには影響しません。TypeScriptなら型定義を同梱するnpmパッケージをそのまま使えます。
import { GenericContainer } from "testcontainers";
const redis = await new GenericContainer("redis:7.4")
.withExposedPorts(6379)
.start();
const port = redis.getMappedPort(6379);
モジュールの数は言語ごとに差があり、Python 4.15.0のcommunity配下は45種類でした。目的のクラスが無ければ、汎用のコンテナクラスにイメージ名とポートを渡して起動します。
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に2.0系のpomはありません。「バージョンを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は1つずつ書き換えが必要です。
コンテナクラスの移動とジェネリクスの除去
クラスの移動には互換性が残されています。testcontainers-mysql:2.0.5のjarには新旧2つのMySQLContainerが同梱され、旧org.testcontainers.containers.MySQLContainerには@Deprecatedが付いています。importを直さなくても当面は動きます。
見落としやすいのは、新クラスでジェネリクスが外れている点です。旧クラスはMySQLContainer<SELF extends MySQLContainer<SELF>>という自己参照型でしたが、新クラスはpublic class MySQLContainer extends JdbcDatabaseContainer<MySQLContainer>と宣言されています。MySQLContainer<?>と書いていたフィールドはMySQLContainerへ直す必要があり、この差はリリースノートに載っていません。Spring Bootでのバージョン別の書き方は@ServiceConnectionとは|Spring BootのTestcontainers接続を自動化する書き方と@DynamicPropertySourceとの違いにあります。
1.x系から上げるときの確認順序
一度に全部を書き換えると、どの変更で落ちたのか分からなくなります。次の順で進めるとビルドエラーの原因を切り分けられます。
- JUnit 4のテストが残っていないか確認する。残っていれば2.0へ上げる前にJUnit 5へ移す
- 依存のアーティファクトIDへ
testcontainers-接頭辞を付ける - 移行先が非ジェネリック型の場合に限り、
MySQLContainer<?>などの型引数を外す - importの非推奨警告を新パッケージへ寄せる
KafkaのConfluentKafkaContainerは1.20.2のjarに、後述するファイルコピー用メソッドの非推奨化は1.19.8にすでに入っており、どちらも2.0固有の作業ではありません。
Docker Desktop以外で動かすときの設定
Colimaで3言語が失敗した原因と必要な環境変数
公式ドキュメントは実行環境としてDocker Desktop、Linux上のDocker、Testcontainers Cloud、Colima、Podman、Rancher Desktopを挙げつつ、代替ランタイムは「主要な開発ワークフローでは能動的にテストしていない」と断っています。Colima上のDocker 29.5.2で、上のJava・Python・Goのコードを環境変数の組み合わせを変えて実行しました。docker contextはColimaを指している状態です。
| 設定 | Java 2.0.5 | Python 4.15.0 | Go v0.44.0 |
|---|---|---|---|
| 環境変数なし | Dockerを検出できず失敗 | Ryukの起動で失敗 | Dockerを検出できず失敗 |
| DOCKER_HOSTのみ | Ryukの起動で失敗 | Ryukの起動で失敗 | Ryukの起動で失敗 |
| DOCKER_HOSTとソケットの上書き | 成功 | 成功 | 成功 |
失敗は2種類に分かれます。1つ目は接続先が見つからないもので、JavaとGoはdocker contextの設定を読まず、それぞれ「Could not find a valid Docker environment」「rootless Docker not found, failed to create Docker provider」で止まりました。Pythonだけは現在のコンテキストを読んでColimaへ接続できました。
2つ目はRyukの起動失敗で、3言語とも「error while creating mount source path ‘…/.colima/default/docker.sock’」という同じエラーでした。RyukはDockerのソケットをコンテナ内へマウントして動きますが、Mac側のソケットのパスをそのまま渡すため、Colimaの仮想マシン内に無いパスとなって失敗します。仮想マシン内のソケットの位置を教えるのがTESTCONTAINERS_DOCKER_SOCKET_OVERRIDEです。
export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock"
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock
この2行で3言語とも通りました。Pythonはソケットの上書きだけでも通ります。公式の対応環境のページは、これに加えてcolima start --network-addressで起動し、TESTCONTAINERS_HOST_OVERRIDEへ仮想マシンのアドレスを入れる手順も示しています。今回はポート転送でlocalhostから接続できたため不要で、届かない構成のときに足す設定です。公式手順でソケットの上書きを指定するのは、macOS上のPodmanと、macOS(M1)上で管理者権限を使わないRancher DesktopのVZ構成です。後者ではTESTCONTAINERS_HOST_OVERRIDEも指定します。Linux上のPodmanや、管理者権限で動かすRancher Desktopは設定が異なります。Colima自体の導入はColimaとは?MacでDocker Desktopを外す導入手順と設定の落とし穴【0.10系】で解説しています。
.testcontainers.propertiesと環境変数の優先順位
Java版の設定は、ホームディレクトリの.testcontainers.propertiesとクラスパス上のtestcontainers.propertiesの2箇所に書けます。優先順位は環境変数、ホームの設定ファイル、クラスパスの順です。環境変数名は原則としてプロパティ名を大文字とアンダースコアに直してTESTCONTAINERS_を付けた形で、checks.disableならTESTCONTAINERS_CHECKS_DISABLEになります。接続先のdocker.hostは例外で、環境変数ではDOCKER_HOSTを使います。
# ~/.testcontainers.properties
docker.host=unix:///Users/myuser/.colima/default/docker.sock
checks.disable=true
pull.timeout=180
CIで効かせたい設定は環境変数、Colimaのソケットのように人によって違う値はホームの設定ファイルに置き、クラスパス側へコミットしないでください。
テストを不安定にする書き方と置き換え先
Docker社のブログ「Testcontainersのベストプラクティス」(2023年11月)は、テストが環境によって落ちる原因になる書き方を挙げています。そのうち6つを、Java 2.0.5のソースで現在の推奨と照らし合わせると次のようになります。
- ホスト側のポートを固定する:並列実行やCIで衝突します。
getMappedPort()で割り当て後のポートを受け取ります - 接続先を
localhostと直書きする:リモートのDockerやColimaの一部の構成で届きません。getHost()を使います - コンテナ名を固定する:同じマシンで2本のパイプラインが走ると名前が衝突します
- ホストのファイルをバインドマウントする:リモートのDockerやTestcontainers Cloudではホストのパスが見えません。2.0.5では
withFileSystemBindだけでなく、ブログが推奨していたwithCopyFileToContainerも@Deprecatedで、置き換え先はwithCopyToContainer(Transferable, String)です。MountableFileはTransferableを実装しているので、引数はそのまま渡せます Thread.sleep()で起動を待つ:遅いマシンでは足りず、速いマシンでは無駄になります。Wait.forLogMessage()などの待機戦略に置き換えます- イメージタグを
latestにする:コードを変えていないのに別のメジャーバージョンが降ってきて、ある日突然テストが落ちます。mysql:8.4のように本番と同じバージョン系列を指定します。ただしタグの参照先は更新されるため、同一イメージの再現が必要なら、本番と同じダイジェストまで固定します
CI環境でTestcontainersを動かす構成
ソケットマウントによる基本形
Dockerを使ったテスト自動化をCIへ載せるとき、最初に決めるのはデーモンの与え方です。公式が主に推奨するのは、CIジョブのコンテナからホストのDockerデーモンを使い、テスト対象のコンテナを兄弟として起動する構成です。テスト対象コンテナへホストのファイルをバインドマウントする場合には、パスに関する追加条件があり、公式は「ローカルのソースコードディレクトリは、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でビルド・自動テストを設定する方法|build.ymlの書き方と失敗の直し方で扱っています。CIの設計からテストの自動化まで外部の手を借りたい場合は、DevOps・CI/CD導入支援で相談を受け付けています。
Docker in Dockerを選ぶ条件
公式はDocker in Dockerについて「一般に最後の手段と見なされるが、一部のCI環境では必要になる」と書いています。ソケットを共有できない制約のあるCIサービスで初めて選ぶ構成で、イメージのキャッシュがジョブごとに失われやすく、ビルド時間でも不利です。前節のソケットマウント構成をDocker Desktop上で使う場合はTESTCONTAINERS_HOST_OVERRIDEにhost.docker.internalを設定します。
テスト起動時間の削り方と並列実行の制約
シングルトンコンテナによる起動回数の削減
staticで共有できるのはクラスの中までです。テストクラスが増えると、クラスごとの起動が積み上がります。抽象基底クラスの静的初期化ブロックで1回だけ起動し、各テストクラスがそれを継承すると、起動はJVMあたり1回で済みます。
import static org.junit.jupiter.api.Assertions.assertTrue;
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 起動済みのコンテナを共有できる() {
assertTrue(MY_SQL_CONTAINER.isRunning());
}
}
この形でも停止処理は書きません。公式は「テストスイートの終了時に、Testcontainersのコアが起動したRyukコンテナがシングルトンコンテナの停止を引き受ける」と説明しています。代わりに必要になるのがテスト間のデータの分離で、各テストの冒頭でテーブルを初期化します。
JUnitの並列実行とテストデータの分離
クラスを並列に走らせたくなったら、まず公式の制約を確認してください。JUnit 5拡張のドキュメントは「この拡張は逐次実行でしかテストしておらず、並列実行での利用はサポート外で、意図しない副作用が起こりうる」と明記しています。@Testcontainers(parallel = true)という指定もありますが、これは1つのクラス内の複数コンテナを並行して起動するためのもので、既定値はfalseです。テストの並列実行とは別の話です。
並列化するなら、@Containerに頼らず上のシングルトンでコンテナを1つ起動し、データの置き場所をスレッドごとに分けます。PostgreSQLなら、マイグレーション済みのデータベースを雛形にしてCREATE DATABASE ... TEMPLATEでスレッドごとの複製を作れます。ただし複製元DBへの接続を閉じ、複製先にはスレッドごとに異なる名前を指定する必要があります。1つのデータベースを共有したままでは、アプリ側が別の接続でコミットする処理を含むとロールバックで戻せず、他スレッドのテストへデータが漏れます。
コンテナ再利用の有効化手順とCIでの制約
テストの実行をまたいでコンテナを残すのが再利用機能です。コンテナ側にwithReuse(true)を指定し、start()を手動で呼び、さらに環境変数TESTCONTAINERS_REUSE_ENABLE=trueかホームの設定ファイルのtestcontainers.reuse.enable=trueで有効にします。クラスパス上の設定ファイルでは有効になりません。リポジトリへのコミットだけでCIの挙動が変わらないようにするための仕様です。
公式は再利用を「まだ実験的機能」と位置づけ、「すべてのテストが終わっても停止しない」「CI環境には適さない」と続けています。CIで残ったコンテナは次のジョブへ古いデータを持ち込むので、使うのは開発者のローカル環境に限ってください。
Testcontainersを採用しない方がよい場面
すべてのテストをTestcontainersに寄せる方針は失敗します。効くのは、外部ミドルウェアとの境界を検証する層に絞ったときです。次の条件に当てはまる場合は、別の手段を先に検討してください。
- Dockerデーモンに触れないCI環境。共有ランナーでソケットが非公開、特権コンテナも禁止なら、CIサービス側のサービスコンテナ機能かTestcontainers Cloud(現在はDockerの料金プランに組み込まれている)を検討する
- 純粋なロジックの単体テスト。数ミリ秒で終わるテストに、1回十数秒かかることもあるコンテナ起動を挟む理由はない
- コンテナ版が存在しない外部サービス。決済代行のようなSaaSが相手なら、契約テストやスタブサーバーのほうが現実的
- Node.jsでPostgreSQLだけを使うテスト。Dockerを使わずプロセス内でPostgreSQLを動かすPGliteとは?Docker不要のPostgreSQLの使い方とpgvectorでの類似度検索【0.5系】で足りる場合がある
- 用意済みの検証環境に対して画面操作だけを確かめるテスト。DBやアプリをテスト側で起動しないなら、コンテナ管理の出番はない。ブラウザ操作の設計はE2Eテストとは?結合テストとの違いとエンドツーエンドテストのベストプラクティス【Playwright 1.64で実測】のレイヤの話になる
Docker Desktopのライセンス条件も導入前に確認してください。Dockerのライセンス案内は、商用利用が無償になる範囲を「従業員250人未満かつ年間売上1,000万ドル未満の小規模事業者」としており、この無償条件を満たさない商用利用にはPro・Team・Businessのいずれかの契約が必要です(2026年11月3日時点)。満たせないなら、上の手順でColimaやPodmanでの動作を先に確かめておくと移行が楽です。
よくある質問
testcontainers.propertiesはどこに置きますか?
ホームディレクトリ直下の.testcontainers.properties(先頭がドット)と、クラスパス上のtestcontainers.propertiesの2箇所です。コンテナ再利用の有効化のように、ホーム側でしか受け付けない項目もあります。
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDEはいつ必要ですか?
Dockerデーモンが仮想マシンの中で動き、Mac側から見えるソケットのパスと仮想マシン内のパスが違う環境で必要です。Colimaでは、指定しないとJava・Python・GoのいずれもRyukの起動時に「error while creating mount source path」で失敗し、/var/run/docker.sockを指定すると通りました。公式が同じソケット値を指定しているのは、macOS上のPodmanと、macOS(M1)上で管理者権限を使わないRancher DesktopのVZ構成です。
テストが遅いときは何から確認しますか?
切り分けの順序は、イメージの取得、起動回数、再利用の3段です。初回だけ遅いならイメージのダウンロードが原因で、手元ではmysql:8.4の取得に28秒かかりました。毎回遅いならフィールドがstaticかを確認し、クラス数が多ければシングルトンへ寄せます。再利用はその後、ローカル環境に限って検討します。
テスト終了後にstop()を呼ぶ必要はありますか?
@Containerを使っている場合は不要です。JUnit 5の拡張が停止させ、取りこぼしはRyukが回収します。自分でstart()を呼ぶシングルトンでも、テストスイート終了時にRyukが停止させます。TESTCONTAINERS_RYUK_DISABLED=trueでRyukを止めている環境でもJUnit拡張による停止は行われますが、手動で起動したシングルトンはJVM終了時のクリーンアップだけが頼りになります。
Testcontainers 2.0へ上げるとimport文は全部書き直しですか?
書き直しが必須なのは依存のアーティファクトIDです。org.testcontainers:mysqlには2.0系が存在しないため、testcontainers-mysqlへ変えないとビルドが通りません。import文は旧クラスがjarに残っているため警告が出るだけで動きますが、新クラスへ移すならMySQLContainer<?>の型宣言も直します。