testcontainers-go について
testcontainers-goは、Dockerコンテナ内の実サービスを使った統合テストを書くGo開発者向けのガイドです。事前設定済みモジュールや汎用コンテナに加え、ネットワーク、起動待機、リソースのクリーンアップを扱います。
主な機能
- 01事前設定済みサービスモジュールを利用
- 02テスト内でコンテナを起動・管理
- 03ネットワーク、ボリューム、環境変数を設定
- 04サービスの起動完了を待機
- 05コンテナのクリーンアップを管理
活用シーン
- 01実際のデータベースやメッセージキューでテスト
- 02依存サービスの複数バージョンでテスト
- 03再現可能なテスト環境を作成
- 04統合テストで外部依存のモックを避ける
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ freightcognition/ linehaulai-claude-marketplace --skill testcontainers-goインストール後、Claude Code で/testcontainers-goを入力して読み込まれたか確認します。
詳細
Go の統合テスト向け Testcontainers
Go プロジェクトで Docker コンテナを使い、信頼性の高い統合テストを作成するための Testcontainers for Go の包括的なガイドです。
説明
このスキルでは、Go のライブラリである Testcontainers for Go を使った統合テストの作成方法を説明します。このライブラリを使うと、一般的なデータベース、メッセージキュー、ウェブブラウザー、その他 Docker コンテナで実行できるものの、軽量で使い捨て可能なインスタンスを利用できます。
主な機能:
- 一般的なサービス(データベース、メッセージキュー、クラウドサービスなど)向けの、事前構成済みモジュールを 62 種類以上利用できます
- Go のテストで Docker コンテナをセットアップして管理できます
- ネットワーク、ボリューム、環境変数を構成できます
- 適切なクリーンアップとリソース管理を実装できます
- コンテナの問題をデバッグしてトラブルシューティングできます
このスキルを使う場面
次のような場合に、このスキルを使います。
- 実際のサービス(データベース、メッセージキューなど)が必要な統合テストを作成する場合
- 依存関係の複数のバージョンや構成に対してテストする場合
- 再現可能なテスト環境を作成する場合
- 統合テストで外部依存関係をモックするのを避ける場合
- 一時的なテスト用インフラストラクチャをセットアップする場合
前提条件
- Docker または Podman がインストールされ、実行中であること
- Go 1.24+(プロジェクト固有の要件は
go.modを確認してください) - Docker ソケットが標準の場所からアクセス可能であること(macOS/Windows では Docker Desktop、Linux では
/var/run/docker.sock)
手順
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)
defer db.Close()
// Create initial data
db.Exec("CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
db.Exec("INSERT INTO users (name) VALUES ('Alice')")
// Take snapshot
err = pgContainer.Snapshot(ctx, postgres.WithSnapshotName("initial"))
require.NoError(t, err)
// Make changes
db.Exec("INSERT INTO users (name) VALUES ('Bob')")
// Restore to snapshot
err = pgContainer.Restore(ctx, postgres.WithSnapshotName("initial"))
require.NoError(t, err)
// Bob is gone, only Alice remains
}
// Kafka: Get bootstrap servers
kafkaContainer, _ := kafka.Run(ctx, "confluentinc/confluent-local:7.5.0")
brokers, _ := kafkaContainer.Brokers(ctx)
適切なモジュールを見つける
- 利用可能なモジュールを確認する:https://testcontainers.com/modules/?language=go(完全かつ最新の一覧)
- モジュールディレクトリを確認する: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 の多重化ヘッダーを取り除きます
- 標準出力と標準エラー出力を、クリーンな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
例
例 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