mcpField
T
testcontainers-go

testcontainers-go

github.com/testcontainersテスト・品質2040 回インストール34 週間前に更新
リポジトリ

testcontainers-go について

Testcontainers for Go を使った統合テストの作成を Go 開発者向けに支援するスキルです。コンテナ設定、サービスモジュール、テストの信頼性、クリーンアップを扱います。

主な機能

  1. 01事前設定済みサービスモジュールの利用ガイド
  2. 02汎用コンテナのセットアップ
  3. 03コンテナネットワークと設定
  4. 04待機戦略とリソースのクリーンアップ
  5. 05Go のテスト規約

活用シーン

  1. 01実データベースやキューを使ったテスト
  2. 02複数の依存バージョンのテスト
  3. 03再現可能なテスト環境の作成
  4. 04CI での一時サービスの準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/testcontainers/claude-skills --skill testcontainers-go

インストール後、Claude Code で/testcontainers-goを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE
examples/
01_postgres_basic_test.go
02_postgres_snapshot_test.go
03_redis_cache_test.go
04_multi_container_network_test.go
05_generic_container_test.go
06_ci_patterns_test.go
go.mod
go.sum
README.md

詳細

Go の統合テスト向け Testcontainers

Testcontainers を使った統合テストを専門とする、Go のエキスパート開発者です。このスキルが有効な場合は、次の点に従ってください。

  • 汎用コンテナよりも、事前設定済みのモジュールを常に優先する(モジュールが存在する場合)
  • 作成するすべてのテストで、エラーチェックより先にクリーンアップするパターンに従う
  • time.Sleep() ではなく、適切な待機戦略を使用する — 同期手段として time.Sleep() を提案しない
  • 必要なインポートをすべて含む、完全で実行可能なテストコードを生成する
  • テーブル駆動テスト、t.Parallel()、ビルドタグ、サブテストなど、Go のテスト規約を適用する

説明

このスキルは、Docker コンテナ内で実行可能な一般的なデータベース、メッセージキュー、ウェブブラウザー、その他のサービスの軽量で使い捨てのインスタンスを提供する Go ライブラリ、Testcontainers for Go を使った統合テストの作成を支援します。

主な機能:

  • 一般的なサービス(データベース、メッセージキュー、クラウドサービスなど)向けの 62 以上の事前設定済みモジュールを使用する
  • Go のテストで Docker コンテナをセットアップして管理する
  • ネットワーク、ボリューム、環境変数を設定する
  • 適切なクリーンアップとリソース管理を実装する
  • コンテナの問題をデバッグしてトラブルシューティングする

このスキルを使用するタイミング

トリガーキーワード: 統合テスト、testcontainers、Docker テスト、コンテナテスト、データベーステスト、Postgres を使ったテスト、Redis を使ったテスト、Kafka を使ったテスト、実データベーステスト、エンドツーエンドテストのインフラストラクチャ、テスト環境のセットアップ、テストのクリーンアップ、テストの分離。

次のような場合にこのスキルを使用します。

  • 実際のサービス(データベース、メッセージキューなど)を必要とする統合テストを作成する
  • 依存関係の複数のバージョンや設定に対してテストする
  • 再現可能なテスト環境を作成する
  • 統合テストで外部依存関係のモックを避ける
  • 一時的なテスト用インフラストラクチャをセットアップする
  • CI/CD パイプライン(GitHub Actions、GitLab CI など)で統合テストを実行する
  • パッケージ内の複数のテストでコンテナのセットアップを共有する

前提条件

  • Docker または Podman がインストールされ、実行中であること
  • Go 1.24 以降(プロジェクト固有の要件は go.mod を確認してください)
  • Docker ソケットが標準の場所からアクセスできること(macOS/Windows では Docker Desktop、Linux では /var/run/docker.sock)

判断ガイド

この判断ツリーを使用して、適切なアプローチを選択してください。

Need a container for testing?
├── Is there a pre-configured module? (check module list below)
│   ├── YES → Use the module (Section 2)
│   └── NO  → Use a generic container (Section 3)
│
├── Need multiple containers to communicate?
│   └── YES → Create a custom network (Section 5)
│
├── Need shared setup across tests in a package?
│   └── YES → Use TestMain pattern (Section 10)
│
├── Need to separate integration tests from unit tests?
│   └── YES → Use build tags (Section 10)
│
└── Running in CI/CD?
    └── YES → See CI/CD Integration (Section 11)

手順

1. インストールとセットアップ

testcontainers-go をプロジェクトに追加します。

go get github.com/testcontainers/testcontainers-go

事前設定済みのモジュールを使用する場合(推奨):

# Example: PostgreSQL module
go get github.com/testcontainers/testcontainers-go/modules/postgres

# Example: Kafka module
go get github.com/testcontainers/testcontainers-go/modules/kafka

# Example: Redis module
go get github.com/testcontainers/testcontainers-go/modules/redis

Docker が利用可能であることを確認します。

func TestDockerAvailable(t *testing.T) {
    testcontainers.SkipIfProviderIsNotHealthy(t)
    // Test will skip if Docker is not running
}

2. 事前設定済みモジュールの使用(推奨アプローチ)

Testcontainers for Go には、すぐに本番環境で使える設定、妥当なデフォルト値、ヘルパーメソッドを備えた 62 以上の事前設定済みモジュールが用意されています。 利用可能な場合は、汎用コンテナよりも常にモジュールを優先してください。

モジュールを使用する理由
  • 妥当なデフォルト値:事前設定済みのポート、環境変数、待機戦略
  • 接続ヘルパー:ConnectionString()、Endpoint() などの組み込みメソッド
  • 専門機能:モジュール固有の機能(Postgres のスナップショット、Kafka のトピック管理など)
  • 認証情報の自動管理:安全な認証情報の生成と管理
  • 十分に実証済み:数千のプロジェクトで本番環境に使用されています
利用可能なモジュールカテゴリ

データベース(17 モジュール):

  • postgres、mysql、mariadb、mongodb、redis、valkey
  • cockroachdb、clickhouse、memcached、influxdb
  • arangodb、cassandra、scylladb、dynamodb
  • dolt、databend、surrealdb

メッセージキュー(6 モジュール):

  • kafka、rabbitmq、nats、pulsar、redpanda、solace

検索・ベクトルデータベース(9 モジュール):

  • elasticsearch、opensearch、meilisearch
  • weaviate、qdrant、chroma、milvus、vearch、pinecone

クラウド・インフラストラクチャ(6 モジュール):

  • gcloud、azure、azurite、localstack、dind、k3s

サービス・ツール(13 モジュール):

  • consul、etcd、neo4j、couchbase、vault、openldap
  • artemis、inbucket、mockserver、nebulagraph、minio
  • toxiproxy、aerospike

開発(10 モジュール):

  • compose、registry、k6、ollama、grafana-lgtm
  • dockermodelrunner、dockermcpgateway、socat、mssql
基本的なモジュールの使用パターン
package myapp_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestWithPostgres(t *testing.T) {
    ctx := context.Background()

    // Start PostgreSQL container with sensible defaults
    pgContainer, err := postgres.Run(ctx, "postgres:16-alpine")
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    // Get connection string - credentials auto-generated
    connStr, err := pgContainer.ConnectionString(ctx)
    require.NoError(t, err)
    // connStr: "postgres://postgres:password@localhost:49153/postgres?sslmode=disable"

    // Use connection string with your database driver
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    defer db.Close()

    // Run your tests...
}
オプションを使ったモジュールの設定

モジュールは、次の 3 段階でカスタマイズできます。

レベル 1:シンプルなオプション(testcontainers.CustomizeRequestOption 経由)

pgContainer, err := postgres.Run(
    ctx,
    "postgres:16-alpine",
    testcontainers.WithEnv(map[string]string{
        "POSTGRES_DB": "myapp_test",
    }),
    testcontainers.WithLabels(map[string]string{
        "env": "test",
    }),
)

レベル 2:モジュール固有のオプション

// PostgreSQL with init scripts
pgContainer, err := postgres.Run(
    ctx,
    "postgres:16-alpine",
    postgres.WithInitScripts("./testdata/init.sql"),
    postgres.WithDatabase("myapp_test"),
    postgres.WithUsername("custom_user"),
    postgres.WithPassword("custom_pass"),
)

// Redis with configuration
redisContainer, err := redis.Run(
    ctx,
    "redis:7-alpine",
    redis.WithSnapshotting(10, 1),
    redis.WithLogLevel(redis.LogLevelVerbose),
)

// Kafka with custom config
kafkaContainer, err := kafka.Run(
    ctx,
    "confluentinc/confluent-local:7.5.0",
    kafka.WithClusterID("test-cluster"),
)

レベル 3:ライフサイクルフックを使った高度な設定

// PostgreSQL with custom initialization
pgContainer, err := postgres.Run(
    ctx,
    "postgres:16-alpine",
    postgres.WithDatabase("myapp"),
    testcontainers.WithLifecycleHooks(
        testcontainers.ContainerLifecycleHooks{
            PostStarts: []testcontainers.ContainerHook{
                func(ctx context.Context, c testcontainers.Container) error {
                    // Custom initialization after container starts
                    return nil
                },
            },
        },
    ),
)
モジュール固有のヘルパーメソッド

ほとんどのモジュールには、ConnectionString() 以外にも便利なメソッドが用意されています。

// PostgreSQL: Snapshot & Restore for test isolation
func TestDatabaseIsolation(t *testing.T) {
    ctx := context.Background()

    pgContainer, err := postgres.Run(ctx, "postgres:16-alpine")
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    connStr, _ := pgContainer.ConnectionString(ctx)
    db, _ := sql.Open("postgres", connStr)

    // Create initial data
    db.Exec("CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
    db.Exec("INSERT INTO users (name) VALUES ('Alice')")

    // Close connection before snapshot (PostgreSQL can't snapshot with active connections)
    db.Close()

    // Take snapshot
    err = pgContainer.Snapshot(ctx, postgres.WithSnapshotName("initial"))
    require.NoError(t, err)

    // Reconnect and make changes
    db, _ = sql.Open("postgres", connStr)
    db.Exec("INSERT INTO users (name) VALUES ('Bob')")

    // Close connection before restore
    db.Close()

    // Restore to snapshot
    err = pgContainer.Restore(ctx, postgres.WithSnapshotName("initial"))
    require.NoError(t, err)

    // Reconnect — Bob is gone, only Alice remains
    db, _ = sql.Open("postgres", connStr)
    defer db.Close()
}

// Kafka: Get bootstrap servers
kafkaContainer, _ := kafka.Run(ctx, "confluentinc/confluent-local:7.5.0")
brokers, _ := kafkaContainer.Brokers(ctx)
適切なモジュールを見つける
  1. 利用可能なモジュールを閲覧する:https://testcontainers.com/modules/?language=go(完全かつ最新の一覧)
  2. modules ディレクトリを確認する:testcontainers-go GitHub リポジトリ内の /modules/
  3. モジュールのドキュメント:https://golang.testcontainers.org/modules/(各モジュールのオンラインドキュメント)
  4. カテゴリ別に閲覧する(上記の一覧を参照)
  5. サンプルを検索する:各モジュールのディレクトリには examples_test.go があります

モジュールの場所のパターン:

github.com/testcontainers/testcontainers-go/modules/<module-name>

3. 汎用コンテナの使用(代替手段)

事前設定済みのモジュールがない場合は、汎用コンテナを使用してください。

重要:ポートを公開する場合は、必ず待機戦略を追加してください。 これにより、テスト実行前にコンテナの準備が完了していることを確認できます。これは信頼性を確保するうえで不可欠であり、特に CI 環境では重要です。代わりに time.Sleep を使ってはいけません。これはテストが不安定になるアンチパターンです。

func TestCustomContainer(t *testing.T) {
    ctx := context.Background()

    ctr, err := testcontainers.Run(
        ctx,
        "custom-image:latest",
        testcontainers.WithExposedPorts("8080/tcp"),
        testcontainers.WithEnv(map[string]string{
            "APP_ENV": "test",
        }),
        // CRITICAL: Always add wait strategy for exposed ports
        testcontainers.WithWaitStrategy(
            wait.ForListeningPort("8080/tcp").WithStartupTimeout(time.Second*30),
        ),
    )
    testcontainers.CleanupContainer(t, ctr)
    require.NoError(t, err)

    // Get endpoint
    endpoint, err := ctr.Endpoint(ctx, "http")
    require.NoError(t, err)
}

よく使われる汎用コンテナのオプション:

testcontainers.Run(
    ctx,
    "image:tag",

    // Ports
    testcontainers.WithExposedPorts("80/tcp", "443/tcp"),

    // Environment
    testcontainers.WithEnv(map[string]string{
        "KEY": "value",
    }),

    // Files
    testcontainers.WithFiles(testcontainers.ContainerFile{
        Reader:            strings.NewReader("content"),
        ContainerFilePath: "/app/config.yml",
        FileMode:          0o644,
    }),

    // Volumes
    testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) {
        hc.Binds = []string{"/host/path:/container/path"}
    }),

    // Wait strategies (REQUIRED when using WithExposedPorts)
    // Use wait.ForListeningPort for reliability - never use time.Sleep!
    testcontainers.WithWaitStrategy(
        wait.ForListeningPort("80/tcp"),
        // Or use other strategies: wait.ForLog(), wait.ForHTTP(), etc.
    ),

    // Commands
    testcontainers.WithAfterReadyCommand(
        testcontainers.NewRawCommand([]string{"echo", "initialized"}),
    ),

    // Labels
    testcontainers.WithLabels(map[string]string{
        "app": "myapp",
    }),
)

4. 統合テストの作成
テスト構造のベストプラクティス
package myapp_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestDatabaseOperations(t *testing.T) {
    // 1. Setup: Create context
    ctx := context.Background()

    // 2. Start container
    pgContainer, err := postgres.Run(ctx, "postgres:16-alpine")

    // 3. CRITICAL: Register cleanup BEFORE error check
    testcontainers.CleanupContainer(t, pgContainer)

    // 4. Check for errors
    require.NoError(t, err)

    // 5. Get connection details
    connStr, err := pgContainer.ConnectionString(ctx)
    require.NoError(t, err)

    // 6. Connect to service
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    defer db.Close()

    // 7. Run your tests
    err = db.Ping()
    require.NoError(t, err)

    // Test your application logic here...
}

重要なパターン:エラーを確認する前にクリーンアップする

// CORRECT:
ctr, err := testcontainers.Run(ctx, "nginx:alpine")
testcontainers.CleanupContainer(t, ctr)  // Register cleanup immediately
require.NoError(t, err)                   // Then check error

// WRONG: Creates resource leaks
ctr, err := testcontainers.Run(ctx, "nginx:alpine")
require.NoError(t, err)                   // If this fails...
testcontainers.CleanupContainer(t, ctr)  // ...cleanup never registers
コンテナを使ったテーブル駆動テスト
func TestMultipleVersions(t *testing.T) {
    ctx := context.Background()

    versions := []struct {
        name  string
        image string
    }{
        {"Postgres 14", "postgres:14-alpine"},
        {"Postgres 15", "postgres:15-alpine"},
        {"Postgres 16", "postgres:16-alpine"},
    }

    for _, tc := range versions {
        t.Run(tc.name, func(t *testing.T) {
            pgContainer, err := postgres.Run(ctx, tc.image)
            testcontainers.CleanupContainer(t, pgContainer)
            require.NoError(t, err)

            // Run tests against this version...
        })
    }
}
テストの並列実行
func TestParallelContainers(t *testing.T) {
    t.Parallel()  // Enable parallel execution

    ctx := context.Background()

    pgContainer, err := postgres.Run(ctx, "postgres:16-alpine")
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    // Each parallel test gets its own container
}

5. コンテナネットワーク
複数のコンテナの接続
import "github.com/testcontainers/testcontainers-go/network"

func TestMultipleServices(t *testing.T) {
    ctx := context.Background()

    // Create custom network
    nw, err := network.New(ctx)
    testcontainers.CleanupNetwork(t, nw)
    require.NoError(t, err)

    // Start database on network
    pgContainer, err := postgres.Run(
        ctx,
        "postgres:16-alpine",
        network.WithNetwork([]string{"database"}, nw),
    )
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    // Start application on same network
    appContainer, err := testcontainers.Run(
        ctx,
        "myapp:latest",
        testcontainers.WithEnv(map[string]string{
            "DB_HOST": "database",  // Can reach via network alias
            "DB_PORT": "5432",      // Use internal port, not mapped port
        }),
        network.WithNetwork([]string{"app"}, nw),
    )
    testcontainers.CleanupContainer(t, appContainer)
    require.NoError(t, err)

    // Test application can communicate with database...
}
コンテナのポートへのアクセス
func TestPortAccess(t *testing.T) {
    ctx := context.Background()

    ctr, err := testcontainers.Run(
        ctx,
        "nginx:alpine",
        testcontainers.WithExposedPorts("80/tcp"),
    )
    testcontainers.CleanupContainer(t, ctr)
    require.NoError(t, err)

    // Method 1: Get full endpoint (recommended)
    endpoint, err := ctr.Endpoint(ctx, "http")
    require.NoError(t, err)
    // endpoint = "http://localhost:49153"

    // Method 2: Get mapped port only
    port, err := ctr.MappedPort(ctx, "80/tcp")
    require.NoError(t, err)
    portNum := port.Int()  // e.g., 49153

    // Method 3: Get host and port separately
    host, err := ctr.Host(ctx)
    require.NoError(t, err)
    // host = "localhost" (or docker host IP)
}

6. リソース管理とクリーンアップ
クリーンアップ方法

方法 1:testcontainers.CleanupContainer()(推奨)

func TestRecommendedCleanup(t *testing.T) {
    ctx := context.Background()

    ctr, err := testcontainers.Run(ctx, "nginx:alpine")
    testcontainers.CleanupContainer(t, ctr)  // Registers with t.Cleanup
    require.NoError(t, err)

    // Container automatically cleaned up when test ends
}

方法 2:t.Cleanup()(手動)

func TestManualCleanup(t *testing.T) {
    ctx := context.Background()

    ctr, err := testcontainers.Run(ctx, "nginx:alpine")
    require.NoError(t, err)

    t.Cleanup(func() {
        err := testcontainers.TerminateContainer(ctr)
        require.NoError(t, err)
    })
}

方法 3:defer(レガシー)

func TestDeferCleanup(t *testing.T) {
    ctx := context.Background()

    ctr, err := testcontainers.Run(ctx, "nginx:alpine")
    require.NoError(t, err)

    defer func() {
        err := testcontainers.TerminateContainer(ctr)
        require.NoError(t, err)
    }()
}
クリーンアップオプション
// Cleanup with custom timeout
testcontainers.CleanupContainer(t, ctr,
    testcontainers.StopTimeout(10*time.Second),
)

// Cleanup and remove volumes
testcontainers.CleanupContainer(t, ctr,
    testcontainers.RemoveVolumes("volume1", "volume2"),
)

// Combine options
testcontainers.CleanupContainer(t, ctr,
    testcontainers.StopTimeout(5*time.Second),
    testcontainers.RemoveVolumes("data"),
)
Ryuk による自動クリーンアップ

Testcontainers for Go は、テストがクラッシュしたりタイムアウトしたりしてもコンテナを自動的にクリーンアップするガベージコレクター、Ryuk を使用します。

  • サイドカーコンテナ(testcontainers/ryuk:0.13.0)として実行されます
  • テストセッションのライフサイクルを監視します
  • セッション終了時にコンテナをクリーンアップします
  • テストの並列実行に対応します

Ryuk の動作を制御する:

// Disable Ryuk (not recommended)
os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")

// Enable verbose logging
os.Setenv("RYUK_VERBOSE", "true")

// Adjust timeouts
os.Setenv("RYUK_CONNECTION_TIMEOUT", "2m")
os.Setenv("RYUK_RECONNECTION_TIMEOUT", "30s")

7. 設定パターン
環境変数
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithEnv(map[string]string{
        "DATABASE_URL": "postgres://localhost/db",
        "LOG_LEVEL":    "debug",
        "API_KEY":      "test-key",
    }),
)
コンテナ内でのコマンド実行

Exec() でコマンドを実行する場合は、Docker の出力形式を適切に処理するため、exec.Multiplexed() の使用をおすすめします。

import "github.com/testcontainers/testcontainers-go/exec"

// Execute command with Multiplexed option
exitCode, reader, err := ctr.Exec(ctx, []string{"sh", "-c", "echo 'hello'"}, exec.Multiplexed())
require.NoError(t, err)
require.Equal(t, 0, exitCode)

// Read the output
output, err := io.ReadAll(reader)
require.NoError(t, err)
fmt.Println(string(output))

exec.Multiplexed() を使う理由

  • 出力から Docker の多重化ヘッダーを取り除きます
  • stdout と stderr を 1 つのクリーンなストリームにまとめます
  • 出力を読み取り、解析しやすくします

exec.Multiplexed() を使わない場合、解析が難しいヘッダーバイトを含む Docker の未加工の多重化ストリームが出力されます。

ファイルとディレクトリ
// Copy single file
testcontainers.Run(
    ctx,
    "nginx:alpine",
    testcontainers.WithFiles(testcontainers.ContainerFile{
        Reader:            strings.NewReader("server { listen 80; }"),
        ContainerFilePath: "/etc/nginx/conf.d/default.conf",
        FileMode:          0o644,
    }),
)

// Copy multiple files
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithFiles(
        testcontainers.ContainerFile{...},  // config.yml
        testcontainers.ContainerFile{...},  // secrets.json
    ),
)

// Copy from container after start
ctr, _ := testcontainers.Run(ctx, "nginx:alpine")
reader, err := ctr.CopyFileFromContainer(ctx, "/etc/nginx/nginx.conf")
content, _ := io.ReadAll(reader)
ボリュームマウント
testcontainers.Run(
    ctx,
    "postgres:16",
    testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) {
        // Bind mount
        hc.Binds = []string{
            "/host/data:/var/lib/postgresql/data",
        }

        // Named volume
        hc.Mounts = []mount.Mount{
            {
                Type:   mount.TypeVolume,
                Source: "pgdata",
                Target: "/var/lib/postgresql/data",
            },
        }
    }),
)
一時ファイルシステム
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithTmpfs(map[string]string{
        "/tmp":      "rw",
        "/app/temp": "rw,size=100m,mode=1777",
    }),
)

8. 待機戦略

待機戦略は、信頼性の高いテストに欠かせません。 テストの実行前にコンテナーが完全に準備できていることを確認します。タイミングが変動する CI 環境では特に重要です。

ベストプラクティス:

  • ✅ ポートを公開する場合は、常に wait.ForListeningPort() を使用します — これが最も信頼性の高い方法です
  • ✅ サービスに応じて適切な待機戦略(HTTP ヘルスチェック、ログパターンなど)を選択します
  • ❌ time.Sleep() は決して使用しないでください — 不安定なテストにつながるアンチパターンです
  • ✅ CI 環境の低速化に対応できる妥当なタイムアウトを設定します
ポートベースの待機(公開ポートに推奨)
import "github.com/testcontainers/testcontainers-go/wait"

testcontainers.Run(
    ctx,
    "postgres:16",
    testcontainers.WithWaitStrategy(
        wait.ForListeningPort("5432/tcp").
            WithStartupTimeout(30*time.Second).
            WithPollInterval(1*time.Second),
    ),
)
ログベースの待機
testcontainers.Run(
    ctx,
    "elasticsearch:8.7.0",
    testcontainers.WithWaitStrategy(
        wait.ForLog("started").
            WithStartupTimeout(60*time.Second).
            WithOccurrence(1),
    ),
)
HTTP ベースの待機
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithWaitStrategy(
        wait.ForHTTP("/health").
            WithPort("8080/tcp").
            WithStatusCodeMatcher(func(status int) bool {
                return status == 200
            }).
            WithStartupTimeout(30*time.Second),
    ),
)
SQL ベースの待機
testcontainers.Run(
    ctx,
    "postgres:16",
    testcontainers.WithWaitStrategy(
        wait.ForSQL("5432/tcp", "postgres", func(host string, port nat.Port) string {
            return fmt.Sprintf("postgres://user:pass@%s:%s/db?sslmode=disable",
                host, port.Port())
        }).WithStartupTimeout(30*time.Second),
    ),
)
複数の待機戦略
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithWaitStrategy(
        wait.ForAll(
            wait.ForListeningPort("8080/tcp"),
            wait.ForLog("Application started"),
            wait.ForHTTP("/health"),
        ),
    ),
)

9. トラブルシューティング
Docker の利用可否を確認する
func TestDockerConnection(t *testing.T) {
    testcontainers.SkipIfProviderIsNotHealthy(t)

    ctx := context.Background()
    cli, err := testcontainers.NewDockerClientWithOpts(ctx)
    require.NoError(t, err)

    info, err := cli.Info(ctx)
    require.NoError(t, err)

    t.Logf("Docker version: %s", info.ServerVersion)
    t.Logf("OS: %s", info.OperatingSystem)
}
コンテナーログをデバッグする
func TestWithLogging(t *testing.T) {
    ctx := context.Background()

    // Method 1: Stream to stdout
    ctr, _ := testcontainers.Run(
        ctx,
        "myapp:latest",
        testcontainers.WithLogConsumers(
            &testcontainers.StdoutLogConsumer{},
        ),
    )
    testcontainers.CleanupContainer(t, ctr)

    // Method 2: Read logs manually
    rc, _ := ctr.Logs(ctx)
    defer rc.Close()
    logs, _ := io.ReadAll(rc)
    t.Logf("Container logs:\n%s", string(logs))

    // Method 3: Inspect container
    info, _ := ctr.Inspect(ctx)
    t.Logf("Container state: %+v", info.State)
}
よくある問題

問題:コンテナーの起動タイムアウト

// Increase wait timeout
testcontainers.WithWaitStrategy(
    wait.ForListeningPort("5432/tcp").
        WithStartupTimeout(60*time.Second),  // Increase from default
)

// Check logs to see what's happening
testcontainers.WithLogConsumers(&testcontainers.StdoutLogConsumer{})

問題:ポートがすでに使用されています

  • Testcontainers はランダムなポートを自動で割り当てます
  • 必要な場合を除き、ホストポートを手動で指定しないでください
  • 残存しているコンテナーがないか確認します:docker ps -a

問題:イメージのプルに失敗する

# Pull manually first to verify
docker pull postgres:16

# For private registries, login first
docker login registry.example.com
# Testcontainers will use credentials from ~/.docker/config.json

問題:コンテナーがクリーンアップされない

// Verify Ryuk is running
docker ps | grep ryuk

// Check cleanup is registered correctly
testcontainers.CleanupContainer(t, ctr)  // Before error check!
デバッグ用の環境変数
# Enable Ryuk verbose logging
export RYUK_VERBOSE=true

# Adjust timeouts
export RYUK_CONNECTION_TIMEOUT=2m
export RYUK_RECONNECTION_TIMEOUT=30s

# Custom Docker socket
export DOCKER_HOST=unix:///var/run/docker.sock

# Registry prefix for private registry
export TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=private.registry.com

10. 共有セットアップとビルドタグ
統合テスト用のビルドタグ

Go のビルドタグを使用して、統合テストと単体テストを分けます。これにより、明示的に指定しない限り、go test ./... の実行時に統合テストが実行されなくなります。

//go:build integration

package myapp_test

import (
    "context"
    "testing"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestIntegrationWithPostgres(t *testing.T) {
    ctx := context.Background()

    pgContainer, err := postgres.Run(ctx, "postgres:16-alpine")
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    // Integration test logic...
}

統合テストを明示的に実行する:

# Run only integration tests
go test -tags=integration ./...

# Run only unit tests (default, no tag needed)
go test ./...
コンテナーの共有セットアップに使用する TestMain

パッケージ内の複数のテストで同じコンテナーが必要な場合は、TestMain を使ってコンテナーを一度だけ起動し、すべてのテストで共有します。これにより、テストの実行時間を大幅に短縮できます。

package myapp_test

import (
    "context"
    "database/sql"
    "fmt"
    "os"
    "testing"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

var testDB *sql.DB

func TestMain(m *testing.M) {
    ctx := context.Background()

    pgContainer, err := postgres.Run(ctx, "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
    )
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to start postgres: %s\n", err)
        os.Exit(1)
    }

    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to get connection string: %s\n", err)
        os.Exit(1)
    }

    testDB, err = sql.Open("postgres", connStr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to open db: %s\n", err)
        os.Exit(1)
    }

    // Run tests
    code := m.Run()

    // Cleanup
    testDB.Close()
    testcontainers.TerminateContainer(pgContainer)

    os.Exit(code)
}

func TestCreateUser(t *testing.T) {
    // testDB is shared across all tests — no container startup delay
    _, err := testDB.Exec("INSERT INTO users (name) VALUES ($1)", "Alice")
    require.NoError(t, err)
}

func TestGetUser(t *testing.T) {
    var name string
    err := testDB.QueryRow("SELECT name FROM users LIMIT 1").Scan(&name)
    require.NoError(t, err)
}

TestMain とテストごとのコンテナーの使い分け:

  • TestMain:パッケージ内のすべてのテストで同じサービスが必要で、テスト速度が重要な場合
  • テストごとのコンテナー:テストごとに異なる構成が必要な場合、またはテストの分離が重要な場合

11. CI/CD との統合
GitHub Actions
name: Integration Tests
on: [push, pull_request]

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.24'
      - name: Run integration tests
        run: go test -tags=integration -v -timeout 10m ./...

CI に関する主な考慮事項:

  • GitHub Actions の Ubuntu ランナーには Docker があらかじめインストールされています
  • CI でのコンテナー起動に備えて、テストのタイムアウトを延長します(-timeout 10m)
  • デバッグ時は -count=1 を使用してテストキャッシュを無効にします
  • 競合状態の検出には -race フラグの使用を検討します
CI 用の環境構成
// Detect CI environment and adjust timeouts
func getStartupTimeout() time.Duration {
    if os.Getenv("CI") != "" {
        return 120 * time.Second // Longer timeout in CI
    }
    return 30 * time.Second
}

// Use in wait strategy
testcontainers.WithWaitStrategy(
    wait.ForListeningPort("5432/tcp").
        WithStartupTimeout(getStartupTimeout()),
)

アンチパターン

Testcontainers を使用してテストを書く際は、次のよくある間違いを避けてください。

❌ 同期に time.Sleep() を使用する
// WRONG: Flaky, slow, unreliable
ctr, err := testcontainers.Run(ctx, "postgres:16",
    testcontainers.WithExposedPorts("5432/tcp"),
)
time.Sleep(5 * time.Second) // DON'T DO THIS

// CORRECT: Use wait strategies
ctr, err := testcontainers.Run(ctx, "postgres:16",
    testcontainers.WithExposedPorts("5432/tcp"),
    testcontainers.WithWaitStrategy(
        wait.ForListeningPort("5432/tcp").
            WithStartupTimeout(30*time.Second),
    ),
)
❌ クリーンアップを登録する前にエラーを確認する
// WRONG: Resource leak if Run succeeds but error is non-nil
ctr, err := testcontainers.Run(ctx, "nginx:alpine")
require.NoError(t, err)                   // If this panics...
testcontainers.CleanupContainer(t, ctr)   // ...cleanup never runs

// CORRECT: Always register cleanup first
ctr, err := testcontainers.Run(ctx, "nginx:alpine")
testcontainers.CleanupContainer(t, ctr)   // Register cleanup immediately
require.NoError(t, err)                   // Then check error
❌ モジュールがあるのに汎用コンテナーを使用する
// WRONG: Reinventing the wheel
ctr, err := testcontainers.Run(ctx, "postgres:16-alpine",
    testcontainers.WithExposedPorts("5432/tcp"),
    testcontainers.WithEnv(map[string]string{
        "POSTGRES_USER":     "user",
        "POSTGRES_PASSWORD": "pass",
        "POSTGRES_DB":       "mydb",
    }),
    testcontainers.WithWaitStrategy(
        wait.ForLog("database system is ready to accept connections").
            WithOccurrence(2),
    ),
)

// CORRECT: Use the PostgreSQL module
pgContainer, err := postgres.Run(ctx, "postgres:16-alpine",
    postgres.WithDatabase("mydb"),
    postgres.WithUsername("user"),
    postgres.WithPassword("pass"),
)
❌ ポートをハードコードする
// WRONG: Port conflicts, breaks in CI
host, _ := ctr.Host(ctx)
url := fmt.Sprintf("http://%s:8080/api", host) // Hardcoded port!

// CORRECT: Use mapped ports
endpoint, err := ctr.Endpoint(ctx, "http")
url := fmt.Sprintf("%s/api", endpoint) // Uses dynamically assigned port
❌ 公開ポートに対する待機戦略がない
// WRONG: Container may not be ready when tests start
ctr, err := testcontainers.Run(ctx, "myapp:latest",
    testcontainers.WithExposedPorts("8080/tcp"),
    // No wait strategy!
)

// CORRECT: Always add a wait strategy when exposing ports
ctr, err := testcontainers.Run(ctx, "myapp:latest",
    testcontainers.WithExposedPorts("8080/tcp"),
    testcontainers.WithWaitStrategy(
        wait.ForListeningPort("8080/tcp").
            WithStartupTimeout(30*time.Second),
    ),
)

検証チェックリスト

Testcontainers を使用したテストを書いた後、以下の項目を確認してください。

  • エラーチェックより前にクリーンアップを登録 — CleanupContainer(t, ctr) が require.NoError(t, err) より前に記述されています
  • 利用可能な場合はモジュールを使用 — モジュールがあるサービスに汎用コンテナーを使用していません
  • 待機戦略を設定 — すべての WithExposedPorts() に対応する WithWaitStrategy() が指定されています(またはモジュールが待機戦略を提供しています)
  • 同期に time.Sleep() を使用していない — 代わりに待機戦略を使用しています
  • ポートをハードコードしていない — 動的に割り当てられたポートには MappedPort() または Endpoint() を使用しています
  • 適切なインポート — 必要なパッケージ(module、wait、network など)がすべてインポートされています
  • コンテキストを渡す — context.Background() を作成し、コンテナー操作に渡しています
  • リソースを閉じる — データベース接続や Redis クライアントなどを defer で閉じています
  • ビルドタグを使用(任意)— 統合テストファイルに //go:build integration タグを指定しています
  • テストがコンパイルできる — 問題を早期に検出するため、go vet ./... と go build ./... を実行します

例

例 1:PostgreSQL 統合テスト
package myapp_test

import (
    "context"
    "database/sql"
    "testing"

    _ "github.com/lib/pq"
    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestUserRepository(t *testing.T) {
    ctx := context.Background()

    // Start PostgreSQL container
    pgContainer, err := postgres.Run(
        ctx,
        "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
        postgres.WithInitScripts("./testdata/schema.sql"),
    )
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    // Get connection string
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    // Connect to database
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    defer db.Close()

    // Test your repository
    repo := NewUserRepository(db)

    t.Run("CreateUser", func(t *testing.T) {
        user := &User{Name: "Alice", Email: "[email protected]"}
        err := repo.Create(user)
        require.NoError(t, err)
        require.NotZero(t, user.ID)
    })

    t.Run("GetUser", func(t *testing.T) {
        user, err := repo.GetByEmail("[email protected]")
        require.NoError(t, err)
        require.Equal(t, "Alice", user.Name)
    })
}
例 2:Redis キャッシュテスト
package cache_test

import (
    "context"
    "testing"
    "time"

    "github.com/redis/go-redis/v9"
    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/redis"
)

func TestRedisCache(t *testing.T) {
    ctx := context.Background()

    // Start Redis container
    redisContainer, err := redis.Run(
        ctx,
        "redis:7-alpine",
        redis.WithSnapshotting(10, 1),
        redis.WithLogLevel(redis.LogLevelVerbose),
    )
    testcontainers.CleanupContainer(t, redisContainer)
    require.NoError(t, err)

    // Get connection string
    connStr, err := redisContainer.ConnectionString(ctx)
    require.NoError(t, err)

    // Connect to Redis
    opt, err := redis.ParseURL(connStr)
    require.NoError(t, err)

    client := redis.NewClient(opt)
    defer client.Close()

    // Test cache operations
    t.Run("SetAndGet", func(t *testing.T) {
        err := client.Set(ctx, "key1", "value1", time.Minute).Err()
        require.NoError(t, err)

        val, err := client.Get(ctx, "key1").Result()
        require.NoError(t, err)
        require.Equal(t, "value1", val)
    })

    t.Run("Expiration", func(t *testing.T) {
        err := client.Set(ctx, "key2", "value2", time.Second).Err()
        require.NoError(t, err)

        time.Sleep(2 * time.Second)

        _, err = client.Get(ctx, "key2").Result()
        require.Equal(t, redis.Nil, err)
    })
}
例 3:Kafka プロデューサー/コンシューマーテスト
package messaging_test

import (
    "context"
    "testing"
    "time"

    "github.com/segmentio/kafka-go"
    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/kafka"
)

func TestKafkaMessaging(t *testing.T) {
    ctx := context.Background()

    // Start Kafka container
    kafkaContainer, err := kafka.Run(
        ctx,
        "confluentinc/confluent-local:7.5.0",
        kafka.WithClusterID("test-cluster"),
    )
    testcontainers.CleanupContainer(t, kafkaContainer)
    require.NoError(t, err)

    // Get bootstrap servers
    brokers, err := kafkaContainer.Brokers(ctx)
    require.NoError(t, err)

    topic := "test-topic"

    // Create producer
    writer := kafka.NewWriter(kafka.WriterConfig{
        Brokers: brokers,
        Topic:   topic,
    })
    defer writer.Close()

    // Create consumer
    reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers: brokers,
        Topic:   topic,
        GroupID: "test-group",
    })
    defer reader.Close()

    // Test message flow
    t.Run("ProduceAndConsume", func(t *testing.T) {
        // Produce message
        err := writer.WriteMessages(ctx, kafka.Message{
            Key:   []byte("key1"),
            Value: []byte("Hello, Kafka!"),
        })
        require.NoError(t, err)

        // Consume message
        msg, err := reader.ReadMessage(ctx)
        require.NoError(t, err)
        require.Equal(t, "Hello, Kafka!", string(msg.Value))
    })
}
例 4:複数コンテナーのアプリケーションスタック
package integration_test

import (
    "context"
    "net/http"
    "testing"

    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/modules/redis"
    "github.com/testcontainers/testcontainers-go/network"
)

func TestFullStack(t *testing.T) {
    ctx := context.Background()

    // Create custom network
    nw, err := network.New(ctx)
    testcontainers.CleanupNetwork(t, nw)
    require.NoError(t, err)

    // Start PostgreSQL
    pgContainer, err := postgres.Run(
        ctx,
        "postgres:16-alpine",
        network.WithNetwork([]string{"database"}, nw),
    )
    testcontainers.CleanupContainer(t, pgContainer)
    require.NoError(t, err)

    // Start Redis
    redisContainer, err := redis.Run(
        ctx,
        "redis:7-alpine",
        network.WithNetwork([]string{"cache"}, nw),
    )
    testcontainers.CleanupContainer(t, redisContainer)
    require.NoError(t, err)

    // Start application
    appContainer, err := testcontainers.Run(
        ctx,
        "myapp:latest",
        testcontainers.WithEnv(map[string]string{
            "DB_HOST":    "database",
            "DB_PORT":    "5432",
            "REDIS_HOST": "cache",
            "REDIS_PORT": "6379",
        }),
        testcontainers.WithExposedPorts("8080/tcp"),
        network.WithNetwork([]string{"app"}, nw),
    )
    testcontainers.CleanupContainer(t, appContainer)
    require.NoError(t, err)

    // Get application endpoint
    endpoint, err := appContainer.Endpoint(ctx, "http")
    require.NoError(t, err)

    // Test application
    resp, err := http.Get(endpoint + "/health")
    require.NoError(t, err)
    require.Equal(t, 200, resp.StatusCode)
}
例 5:Docker Compose スタック
package compose_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/compose"
)

func TestComposeStack(t *testing.T) {
    ctx := context.Background()

    // Start services from docker-compose.yml
    composeStack, err := compose.NewDockerCompose("./docker-compose.yml")
    require.NoError(t, err)

    t.Cleanup(func() {
        if err := composeStack.Down(ctx); err != nil {
            t.Fatalf("failed to down compose stack: %v", err)
        }
    })

    err = composeStack.Up(ctx, compose.Wait(true))
    require.NoError(t, err)

    // Get service container
    webContainer, err := composeStack.ServiceContainer(ctx, "web")
    require.NoError(t, err)

    // Test service
    endpoint, err := webContainer.Endpoint(ctx, "http")
    require.NoError(t, err)

    // Run tests against the stack...
}

ベストプラクティス

  1. 利用可能な場合は、事前設定済みのモジュールを必ず使用する — 適切なデフォルト値とヘルパーメソッドが用意されています
  2. クリーンアップをすぐに登録する — エラーを確認する前に testcontainers.CleanupContainer(t, ctr) を呼び出します
  3. ポートを公開するときは必ず待機戦略を追加する — 信頼性を高めるため、特に CI では wait.ForListeningPort() を使用します。time.Sleep() は絶対に使用しないでください。不安定なテストを引き起こすアンチパターンです
  4. 適切な待機戦略を選ぶ — ヘルスエンドポイントには wait.ForHTTP()、ログパターンには wait.ForLog()、ポートの利用可能性には wait.ForListeningPort() を使用します
  5. テーブル駆動テストを活用する — 複数のバージョンや設定を対象にテストします
  6. カスタムネットワークを使用する — 複数コンテナー間の通信に使用します
  7. コンテナーを一時的なものとして扱う — テスト間で状態に依存しないでください
  8. Docker の利用可能性を確認する — testcontainers.SkipIfProviderIsNotHealthy(t) を使用します
  9. 並列実行を有効にする — テストスイートを高速化するため、t.Parallel() を使用します
  10. モジュールのヘルパーメソッドを使用する — 例:ConnectionString()、Snapshot()、Restore()
  11. ログでデバッグする — トラブルシューティング時は WithLogConsumers() を使用します

追加リソース

コメント