testcontainers-go について
Testcontainers for Go を使った統合テストの作成を Go 開発者向けに支援するスキルです。コンテナ設定、サービスモジュール、テストの信頼性、クリーンアップを扱います。
主な機能
- 01事前設定済みサービスモジュールの利用ガイド
- 02汎用コンテナのセットアップ
- 03コンテナネットワークと設定
- 04待機戦略とリソースのクリーンアップ
- 05Go のテスト規約
活用シーン
- 01実データベースやキューを使ったテスト
- 02複数の依存バージョンのテスト
- 03再現可能なテスト環境の作成
- 04CI での一時サービスの準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ testcontainers/ claude-skills --skill testcontainers-goインストール後、Claude Code で/testcontainers-goを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEexamples/01_postgres_basic_test.go02_postgres_snapshot_test.go03_redis_cache_test.go04_multi_container_network_test.go05_generic_container_test.go06_ci_patterns_test.gogo.modgo.sumREADME.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、valkeycockroachdb、clickhouse、memcached、influxdbarangodb、cassandra、scylladb、dynamodbdolt、databend、surrealdb
メッセージキュー(6 モジュール):
kafka、rabbitmq、nats、pulsar、redpanda、solace
検索・ベクトルデータベース(9 モジュール):
elasticsearch、opensearch、meilisearchweaviate、qdrant、chroma、milvus、vearch、pinecone
クラウド・インフラストラクチャ(6 モジュール):
gcloud、azure、azurite、localstack、dind、k3s
サービス・ツール(13 モジュール):
consul、etcd、neo4j、couchbase、vault、openldapartemis、inbucket、mockserver、nebulagraph、miniotoxiproxy、aerospike
開発(10 モジュール):
compose、registry、k6、ollama、grafana-lgtmdockermodelrunner、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)
適切なモジュールを見つける
- 利用可能なモジュールを閲覧する:https://testcontainers.com/modules/?language=go(完全かつ最新の一覧)
- modules ディレクトリを確認する:testcontainers-go GitHub リポジトリ内の
/modules/ - モジュールのドキュメント:https://golang.testcontainers.org/modules/(各モジュールのオンラインドキュメント)
- カテゴリ別に閲覧する(上記の一覧を参照)
- サンプルを検索する:各モジュールのディレクトリには
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...
}
ベストプラクティス
- 利用可能な場合は、事前設定済みのモジュールを必ず使用する — 適切なデフォルト値とヘルパーメソッドが用意されています
- クリーンアップをすぐに登録する — エラーを確認する前に
testcontainers.CleanupContainer(t, ctr)を呼び出します - ポートを公開するときは必ず待機戦略を追加する — 信頼性を高めるため、特に CI では
wait.ForListeningPort()を使用します。time.Sleep()は絶対に使用しないでください。不安定なテストを引き起こすアンチパターンです - 適切な待機戦略を選ぶ — ヘルスエンドポイントには
wait.ForHTTP()、ログパターンにはwait.ForLog()、ポートの利用可能性にはwait.ForListeningPort()を使用します - テーブル駆動テストを活用する — 複数のバージョンや設定を対象にテストします
- カスタムネットワークを使用する — 複数コンテナー間の通信に使用します
- コンテナーを一時的なものとして扱う — テスト間で状態に依存しないでください
- Docker の利用可能性を確認する —
testcontainers.SkipIfProviderIsNotHealthy(t)を使用します - 並列実行を有効にする — テストスイートを高速化するため、
t.Parallel()を使用します - モジュールのヘルパーメソッドを使用する — 例:
ConnectionString()、Snapshot()、Restore() - ログでデバッグする — トラブルシューティング時は
WithLogConsumers()を使用します
追加リソース
- 公式ドキュメント:https://golang.testcontainers.org/
- 利用可能なモジュール:https://testcontainers.com/modules/?language=go (完全かつ最新の一覧)
- モジュールのドキュメント:https://golang.testcontainers.org/modules/ (各モジュールのオンラインドキュメント)
- GitHub リポジトリ:https://github.com/testcontainers/testcontainers-go
- モジュールの例:GitHub リポジトリ内の
modules/*/examples_test.goファイルを確認してください - コミュニティ Slack:testcontainers.slack.com