commit 7d21263064519424a662367d81a727bba547ce49 Author: Zhongwei Li Date: Sun Nov 30 09:01:35 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..c7440f3 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "testcontainers-go", + "description": "A comprehensive guide for using Testcontainers for Go to write reliable integration tests with Docker containers in Go projects. Supports 62+ pre-configured modules for databases, message queues, cloud services, and more.", + "version": "1.0.0", + "author": { + "name": "Testcontainers", + "email": "info@testcontainers.org" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1d230e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Testcontainers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..091deab --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# testcontainers-go + +A comprehensive guide for using Testcontainers for Go to write reliable integration tests with Docker containers in Go projects. Supports 62+ pre-configured modules for databases, message queues, cloud services, and more. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..bc52c06 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1253 @@ +--- +name: testcontainers-go +description: A comprehensive guide for using Testcontainers for Go to write reliable integration tests with Docker containers in Go projects. Supports 62+ pre-configured modules for databases, message queues, cloud services, and more. +license: MIT +--- + +# Testcontainers for Go Integration Testing + +A comprehensive guide for using Testcontainers for Go to write reliable integration tests with Docker containers in Go projects. + +## Description + +This skill helps you write integration tests using Testcontainers for Go, a Go library that provides lightweight, throwaway instances of common databases, message queues, web browsers, or anything that can run in a Docker container. + +**Key capabilities:** +- Use 62+ pre-configured modules for common services (databases, message queues, cloud services, etc.) +- Set up and manage Docker containers in Go tests +- Configure networking, volumes, and environment variables +- Implement proper cleanup and resource management +- Debug and troubleshoot container issues + +## When to Use This Skill + +Use this skill when you need to: +- Write integration tests that require real services (databases, message queues, etc.) +- Test against multiple versions or configurations of dependencies +- Create reproducible test environments +- Avoid mocking external dependencies in integration tests +- Set up ephemeral test infrastructure + +## Prerequisites + +- **Docker or Podman** installed and running +- **Go 1.24+** (check `go.mod` for project-specific requirements) +- **Docker socket** accessible at standard locations (Docker Desktop on macOS/Windows, `/var/run/docker.sock` on Linux) + +## Instructions + +### 1. Installation & Setup + +Add testcontainers-go to your project: + +```bash +go get github.com/testcontainers/testcontainers-go +``` + +For pre-configured modules (recommended): + +```bash +# 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 +``` + +**Verify Docker availability:** + +```go +func TestDockerAvailable(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + // Test will skip if Docker is not running +} +``` + +--- + +### 2. Using Pre-Configured Modules (Recommended Approach) + +**Testcontainers for Go provides 62+ pre-configured modules** that offer production-ready configurations, sensible defaults, and helper methods. **Always prefer modules over generic containers** when available. + +#### Why Use Modules? + +- **Sensible defaults**: Pre-configured ports, environment variables, and wait strategies +- **Connection helpers**: Built-in methods like `ConnectionString()`, `Endpoint()` +- **Specialized features**: Module-specific functionality (e.g., Postgres snapshots, Kafka topic management) +- **Automatic credentials**: Secure credential generation and management +- **Battle-tested**: Used in production by thousands of projects + +#### Available Module Categories + +**Databases (17 modules):** +- `postgres`, `mysql`, `mariadb`, `mongodb`, `redis`, `valkey` +- `cockroachdb`, `clickhouse`, `memcached`, `influxdb` +- `arangodb`, `cassandra`, `scylladb`, `dynamodb` +- `dolt`, `databend`, `surrealdb` + +**Message Queues (6 modules):** +- `kafka`, `rabbitmq`, `nats`, `pulsar`, `redpanda`, `solace` + +**Search & Vector Databases (9 modules):** +- `elasticsearch`, `opensearch`, `meilisearch` +- `weaviate`, `qdrant`, `chroma`, `milvus`, `vearch`, `pinecone` + +**Cloud & Infrastructure (6 modules):** +- `gcloud`, `azure`, `azurite`, `localstack`, `dind`, `k3s` + +**Services & Tools (13 modules):** +- `consul`, `etcd`, `neo4j`, `couchbase`, `vault`, `openldap` +- `artemis`, `inbucket`, `mockserver`, `nebulagraph`, `minio` +- `toxiproxy`, `aerospike` + +**Development (10 modules):** +- `compose`, `registry`, `k6`, `ollama`, `grafana-lgtm` +- `dockermodelrunner`, `dockermcpgateway`, `socat`, `mssql` + +#### Basic Module Usage Pattern + +```go +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... +} +``` + +#### Module Configuration with Options + +Modules support three levels of customization: + +**Level 1: Simple Options (via testcontainers.CustomizeRequestOption)** + +```go +pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + testcontainers.WithEnv(map[string]string{ + "POSTGRES_DB": "myapp_test", + }), + testcontainers.WithLabels(map[string]string{ + "env": "test", + }), +) +``` + +**Level 2: Module-Specific Options** + +```go +// 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"), +) +``` + +**Level 3: Advanced Configuration with Lifecycle Hooks** + +```go +// 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 + }, + }, + }, + ), +) +``` + +#### Module-Specific Helper Methods + +Most modules provide convenience methods beyond `ConnectionString()`: + +```go +// 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) +``` + +#### Finding the Right Module + +1. **Browse available modules**: https://testcontainers.com/modules/?language=go (complete, up-to-date list) +2. **Check the modules directory**: `/modules/` in the [testcontainers-go GitHub repository](https://github.com/testcontainers/testcontainers-go) +3. **Module documentation**: https://golang.testcontainers.org/modules/ (online docs for each module) +4. **Browse by category** (see lists above) +5. **Search for examples**: Each module has `examples_test.go` in its directory + +**Module location pattern:** +``` +github.com/testcontainers/testcontainers-go/modules/ +``` + +--- + +### 3. Using Generic Containers (Fallback) + +When no pre-configured module exists, use generic containers. + +**IMPORTANT: Always add a wait strategy when exposing ports** to ensure the container is ready before tests run. This is critical for reliability, especially in CI environments. Never use `time.Sleep` as a substitute - it's an anti-pattern that leads to flaky tests. + +```go +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) +} +``` + +**Common generic container options:** + +```go +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. Writing Integration Tests + +#### Test Structure Best Practices + +```go +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... +} +``` + +**Critical pattern: Cleanup BEFORE error checking** + +```go +// 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 +``` + +#### Table-Driven Tests with Containers + +```go +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... + }) + } +} +``` + +#### Parallel Test Execution + +```go +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. Container Networking + +#### Connecting Multiple Containers + +```go +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... +} +``` + +#### Accessing Container Ports + +```go +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. Resource Management & Cleanup + +#### Cleanup Methods + +**Method 1: `testcontainers.CleanupContainer()` (Recommended)** + +```go +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 +} +``` + +**Method 2: `t.Cleanup()` (Manual)** + +```go +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) + }) +} +``` + +**Method 3: `defer` (Legacy)** + +```go +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 Options + +```go +// 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"), +) +``` + +#### Automatic Cleanup with Ryuk + +Testcontainers for Go uses **Ryuk**, a garbage collector that automatically cleans up containers even if tests crash or timeout: + +- Runs as a sidecar container (`testcontainers/ryuk:0.13.0`) +- Monitors test session lifecycle +- Cleans up containers when session ends +- Handles parallel test execution + +**Control Ryuk behavior:** + +```go +// 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. Configuration Patterns + +#### Environment Variables + +```go +testcontainers.Run( + ctx, + "myapp:latest", + testcontainers.WithEnv(map[string]string{ + "DATABASE_URL": "postgres://localhost/db", + "LOG_LEVEL": "debug", + "API_KEY": "test-key", + }), +) +``` + +#### Executing Commands in Containers + +When executing commands with `Exec()`, it's recommended to use `exec.Multiplexed()` to properly handle Docker's output format: + +```go +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)) +``` + +**Why use `exec.Multiplexed()`?** +- Removes Docker's multiplexing headers from the output +- Combines stdout and stderr into a single clean stream +- Makes the output easier to read and parse + +Without `exec.Multiplexed()`, you'll get Docker's raw multiplexed stream which includes header bytes that are difficult to parse. + +#### Files and Directories + +```go +// 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) +``` + +#### Volume Mounts + +```go +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", + }, + } + }), +) +``` + +#### Temporary Filesystems + +```go +testcontainers.Run( + ctx, + "myapp:latest", + testcontainers.WithTmpfs(map[string]string{ + "/tmp": "rw", + "/app/temp": "rw,size=100m,mode=1777", + }), +) +``` + +--- + +### 8. Wait Strategies + +**Wait strategies are critical for reliable tests.** They ensure containers are fully ready before tests run, which is especially important in CI environments where timing can vary. + +**Best Practices:** +- ✅ **Always use `wait.ForListeningPort()` when exposing ports** - This is the most reliable approach +- ✅ **Choose appropriate wait strategies** based on your service (HTTP health checks, log patterns, etc.) +- ❌ **Never use `time.Sleep()`** - This is an anti-pattern that leads to flaky tests +- ✅ **Set reasonable timeouts** to handle slow CI environments + +#### Port-Based Waiting (Recommended for Exposed Ports) + +```go +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), + ), +) +``` + +#### Log-Based Waiting + +```go +testcontainers.Run( + ctx, + "elasticsearch:8.7.0", + testcontainers.WithWaitStrategy( + wait.ForLog("started"). + WithStartupTimeout(60*time.Second). + WithOccurrence(1), + ), +) +``` + +#### HTTP-Based Waiting + +```go +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-Based Waiting + +```go +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), + ), +) +``` + +#### Multiple Wait Strategies + +```go +testcontainers.Run( + ctx, + "myapp:latest", + testcontainers.WithWaitStrategy( + wait.ForAll( + wait.ForListeningPort("8080/tcp"), + wait.ForLog("Application started"), + wait.ForHTTP("/health"), + ), + ), +) +``` + +--- + +### 9. Troubleshooting + +#### Check Docker Availability + +```go +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) +} +``` + +#### Debug Container Logs + +```go +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) +} +``` + +#### Common Issues + +**Issue: Container startup timeout** +```go +// 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{}) +``` + +**Issue: Port already in use** +- Testcontainers auto-assigns random ports +- Don't manually specify host ports unless necessary +- Check for leaked containers: `docker ps -a` + +**Issue: Image pull failures** +```bash +# 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 +``` + +**Issue: Container not cleaning up** +```go +// Verify Ryuk is running +docker ps | grep ryuk + +// Check cleanup is registered correctly +testcontainers.CleanupContainer(t, ctr) // Before error check! +``` + +#### Environment Variables for Debugging + +```bash +# 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 +``` + +--- + +## Examples + +### Example 1: PostgreSQL Integration Test + +```go +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: "alice@example.com"} + err := repo.Create(user) + require.NoError(t, err) + require.NotZero(t, user.ID) + }) + + t.Run("GetUser", func(t *testing.T) { + user, err := repo.GetByEmail("alice@example.com") + require.NoError(t, err) + require.Equal(t, "Alice", user.Name) + }) +} +``` + +### Example 2: Redis Cache Test + +```go +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) + }) +} +``` + +### Example 3: Kafka Producer/Consumer Test + +```go +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)) + }) +} +``` + +### Example 4: Multi-Container Application Stack + +```go +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) +} +``` + +### Example 5: Docker Compose Stack + +```go +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... +} +``` + +--- + +## Best Practices + +1. **Always use pre-configured modules when available** - They provide sensible defaults and helper methods +2. **Register cleanup immediately** - Call `testcontainers.CleanupContainer(t, ctr)` before checking errors +3. **Always add wait strategies when exposing ports** - Use `wait.ForListeningPort()` to ensure reliability, especially in CI. Never use `time.Sleep()` - it's an anti-pattern that causes flaky tests +4. **Choose appropriate wait strategies** - Use `wait.ForHTTP()` for health endpoints, `wait.ForLog()` for log patterns, or `wait.ForListeningPort()` for port availability +5. **Leverage table-driven tests** - Test against multiple versions or configurations +6. **Use custom networks** - For multi-container communication +7. **Keep containers ephemeral** - Don't rely on state between tests +8. **Check Docker availability** - Use `testcontainers.SkipIfProviderIsNotHealthy(t)` +9. **Enable parallel execution** - Use `t.Parallel()` for faster test suites +10. **Use module helper methods** - E.g., `ConnectionString()`, `Snapshot()`, `Restore()` +11. **Debug with logs** - Use `WithLogConsumers()` when troubleshooting + +--- + +## Additional Resources + +- **Official Documentation**: https://golang.testcontainers.org/ +- **Available Modules**: https://testcontainers.com/modules/?language=go (complete, up-to-date list) +- **Module Documentation**: https://golang.testcontainers.org/modules/ (online docs for each module) +- **GitHub Repository**: https://github.com/testcontainers/testcontainers-go +- **Module Examples**: Check `modules/*/examples_test.go` files in the GitHub repository +- **Community Slack**: [testcontainers.slack.com](https://testcontainers.slack.com) diff --git a/examples/01_postgres_basic_test.go b/examples/01_postgres_basic_test.go new file mode 100644 index 0000000..446ea0a --- /dev/null +++ b/examples/01_postgres_basic_test.go @@ -0,0 +1,126 @@ +package examples_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" +) + +// TestBasicPostgres demonstrates the most basic usage of the PostgreSQL module +func TestBasicPostgres(t *testing.T) { + ctx := context.Background() + + // Start PostgreSQL container with default settings + pgContainer, err := postgres.Run(ctx, "postgres:16-alpine", postgres.BasicWaitStrategies()) + 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() + + // Verify connection + err = db.Ping() + require.NoError(t, err) + + // Run a simple query + var result int + err = db.QueryRow("SELECT 1 + 1").Scan(&result) + require.NoError(t, err) + require.Equal(t, 2, result) + + t.Log("Successfully connected to PostgreSQL and ran a query") +} + +// TestPostgresWithCustomConfig demonstrates using custom database, user, and password +func TestPostgresWithCustomConfig(t *testing.T) { + ctx := context.Background() + + // Start PostgreSQL with custom configuration + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase("testdb"), + postgres.WithUsername("testuser"), + postgres.WithPassword("testpass"), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + require.NoError(t, err) + + // Get connection string + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + // Verify the connection string contains our custom values + require.Contains(t, connStr, "testuser") + require.Contains(t, connStr, "testpass") + require.Contains(t, connStr, "testdb") + + // Connect and verify + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + err = db.Ping() + require.NoError(t, err) + + t.Log("Successfully connected to PostgreSQL with custom configuration") +} + +// TestPostgresWithSchema demonstrates using init scripts to set up a schema +func TestPostgresWithSchema(t *testing.T) { + ctx := context.Background() + + // Note: In a real test, you would create a schema.sql file in testdata/ + // For this example, we'll use WithDatabase and create the table manually + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase("appdb"), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + require.NoError(t, err) + + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Create a simple schema + _, err = db.Exec(` + CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `) + require.NoError(t, err) + + // Insert a test record + _, err = db.Exec(`INSERT INTO users (name, email) VALUES ($1, $2)`, "Alice", "alice@example.com") + require.NoError(t, err) + + // Query the record + var name, email string + err = db.QueryRow(`SELECT name, email FROM users WHERE email = $1`, "alice@example.com").Scan(&name, &email) + require.NoError(t, err) + require.Equal(t, "Alice", name) + require.Equal(t, "alice@example.com", email) + + t.Log("Successfully created schema and inserted data") +} diff --git a/examples/02_postgres_snapshot_test.go b/examples/02_postgres_snapshot_test.go new file mode 100644 index 0000000..7c63d72 --- /dev/null +++ b/examples/02_postgres_snapshot_test.go @@ -0,0 +1,181 @@ +package examples_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" +) + +// TestPostgresSnapshot demonstrates using snapshots for test isolation +// This is useful when you want to run multiple tests against the same initial state +func TestPostgresSnapshot(t *testing.T) { + ctx := context.Background() + + // Start PostgreSQL container with a custom database (required for snapshots) + // Note: Cannot snapshot the default 'postgres' system database + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase("snapshotdb"), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + require.NoError(t, err) + + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Create initial schema and data + _, err = db.Exec(` + CREATE TABLE products ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + price DECIMAL(10, 2) NOT NULL + ) + `) + require.NoError(t, err) + + _, err = db.Exec(`INSERT INTO products (name, price) VALUES ($1, $2)`, "Widget", 9.99) + require.NoError(t, err) + + // Close connection before snapshot (PostgreSQL can't snapshot a database with active connections) + db.Close() + + // Take a snapshot of the initial state + err = pgContainer.Snapshot(ctx, postgres.WithSnapshotName("initial_state")) + require.NoError(t, err) + + t.Log("Snapshot created with initial state") + + // Reconnect to modify the database + db, err = sql.Open("postgres", connStr) + require.NoError(t, err) + + // Modify the database + _, err = db.Exec(`INSERT INTO products (name, price) VALUES ($1, $2)`, "Gadget", 19.99) + require.NoError(t, err) + + // Verify we have 2 products + var count int + err = db.QueryRow(`SELECT COUNT(*) FROM products`).Scan(&count) + require.NoError(t, err) + require.Equal(t, 2, count) + + t.Log("Added second product, count is now 2") + + // Close connection before restore + db.Close() + + // Restore to the snapshot + err = pgContainer.Restore(ctx, postgres.WithSnapshotName("initial_state")) + require.NoError(t, err) + + t.Log("Restored to initial snapshot") + + // Reconnect after restore + db, err = sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Verify we're back to 1 product + err = db.QueryRow(`SELECT COUNT(*) FROM products`).Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count, "After restore, should have only 1 product") + + // Verify it's the original product + var name string + var price float64 + err = db.QueryRow(`SELECT name, price FROM products WHERE id = 1`).Scan(&name, &price) + require.NoError(t, err) + require.Equal(t, "Widget", name) + require.Equal(t, 9.99, price) + + t.Log("Successfully restored to initial state") +} + +// TestPostgresMultipleSnapshots demonstrates using multiple named snapshots +func TestPostgresMultipleSnapshots(t *testing.T) { + ctx := context.Background() + + // Use a custom database name (not 'postgres') for snapshots to work properly + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase("testdb"), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + require.NoError(t, err) + + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Create table + _, err = db.Exec(`CREATE TABLE counters (id INT PRIMARY KEY, value INT)`) + require.NoError(t, err) + + // State 1: Empty table + // Close connection before snapshot (PostgreSQL can't snapshot a database with active connections) + db.Close() + err = pgContainer.Snapshot(ctx, postgres.WithSnapshotName("empty")) + require.NoError(t, err) + t.Log("Snapshot 'empty' created") + + // Reconnect to make changes + db, err = sql.Open("postgres", connStr) + require.NoError(t, err) + + // State 2: One record + _, err = db.Exec(`INSERT INTO counters (id, value) VALUES (1, 10)`) + require.NoError(t, err) + + // Close and snapshot + db.Close() + err = pgContainer.Snapshot(ctx, postgres.WithSnapshotName("one_record")) + require.NoError(t, err) + t.Log("Snapshot 'one_record' created") + + // Reconnect to make changes + db, err = sql.Open("postgres", connStr) + require.NoError(t, err) + + // State 3: Two records + _, err = db.Exec(`INSERT INTO counters (id, value) VALUES (2, 20)`) + require.NoError(t, err) + + // Close and snapshot + db.Close() + err = pgContainer.Snapshot(ctx, postgres.WithSnapshotName("two_records")) + require.NoError(t, err) + t.Log("Snapshot 'two_records' created") + + // Now restore to "one_record" state + err = pgContainer.Restore(ctx, postgres.WithSnapshotName("one_record")) + require.NoError(t, err) + + // Reconnect after restore + db, err = sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Verify we have exactly 1 record + var count int + err = db.QueryRow(`SELECT COUNT(*) FROM counters`).Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count, "After restoring to 'one_record', should have 1 record") + + t.Log("Successfully restored to 'one_record' snapshot") +} diff --git a/examples/03_redis_cache_test.go b/examples/03_redis_cache_test.go new file mode 100644 index 0000000..da1cdf1 --- /dev/null +++ b/examples/03_redis_cache_test.go @@ -0,0 +1,191 @@ +package examples_test + +import ( + "context" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + tcredis "github.com/testcontainers/testcontainers-go/modules/redis" +) + +// TestBasicRedis demonstrates basic Redis operations +func TestBasicRedis(t *testing.T) { + ctx := context.Background() + + // Start Redis container + redisContainer, err := tcredis.Run(ctx, "redis:7-alpine") + 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 SET and GET + err = client.Set(ctx, "greeting", "Hello, Testcontainers!", 0).Err() + require.NoError(t, err) + + val, err := client.Get(ctx, "greeting").Result() + require.NoError(t, err) + require.Equal(t, "Hello, Testcontainers!", val) + + t.Log("Successfully performed SET and GET operations") +} + +// TestRedisWithExpiration demonstrates key expiration +func TestRedisWithExpiration(t *testing.T) { + ctx := context.Background() + + redisContainer, err := tcredis.Run(ctx, "redis:7-alpine") + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) + + connStr, err := redisContainer.ConnectionString(ctx) + require.NoError(t, err) + + opt, err := redis.ParseURL(connStr) + require.NoError(t, err) + + client := redis.NewClient(opt) + defer client.Close() + + // Set a key with 2-second expiration + err = client.Set(ctx, "temporary", "I will expire", 2*time.Second).Err() + require.NoError(t, err) + + // Verify key exists + val, err := client.Get(ctx, "temporary").Result() + require.NoError(t, err) + require.Equal(t, "I will expire", val) + + t.Log("Key set with 2-second expiration") + + // Wait for expiration + time.Sleep(3 * time.Second) + + // Verify key is gone + _, err = client.Get(ctx, "temporary").Result() + require.Equal(t, redis.Nil, err, "Key should have expired") + + t.Log("Key successfully expired") +} + +// TestRedisListOperations demonstrates list operations +func TestRedisListOperations(t *testing.T) { + ctx := context.Background() + + redisContainer, err := tcredis.Run(ctx, "redis:7-alpine") + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) + + connStr, err := redisContainer.ConnectionString(ctx) + require.NoError(t, err) + + opt, err := redis.ParseURL(connStr) + require.NoError(t, err) + + client := redis.NewClient(opt) + defer client.Close() + + // Push items to list + err = client.RPush(ctx, "tasks", "task1", "task2", "task3").Err() + require.NoError(t, err) + + // Get list length + length, err := client.LLen(ctx, "tasks").Result() + require.NoError(t, err) + require.Equal(t, int64(3), length) + + // Pop items from list + task, err := client.LPop(ctx, "tasks").Result() + require.NoError(t, err) + require.Equal(t, "task1", task) + + // Get remaining items + tasks, err := client.LRange(ctx, "tasks", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"task2", "task3"}, tasks) + + t.Log("Successfully performed list operations") +} + +// TestRedisHashOperations demonstrates hash operations +func TestRedisHashOperations(t *testing.T) { + ctx := context.Background() + + redisContainer, err := tcredis.Run(ctx, "redis:7-alpine") + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) + + connStr, err := redisContainer.ConnectionString(ctx) + require.NoError(t, err) + + opt, err := redis.ParseURL(connStr) + require.NoError(t, err) + + client := redis.NewClient(opt) + defer client.Close() + + // Set hash fields + err = client.HSet(ctx, "user:1000", map[string]interface{}{ + "name": "John Doe", + "email": "john@example.com", + "age": "30", + }).Err() + require.NoError(t, err) + + // Get single field + name, err := client.HGet(ctx, "user:1000", "name").Result() + require.NoError(t, err) + require.Equal(t, "John Doe", name) + + // Get all fields + user, err := client.HGetAll(ctx, "user:1000").Result() + require.NoError(t, err) + require.Equal(t, "John Doe", user["name"]) + require.Equal(t, "john@example.com", user["email"]) + require.Equal(t, "30", user["age"]) + + t.Log("Successfully performed hash operations") +} + +// TestRedisWithConfig demonstrates using Redis with custom configuration +func TestRedisWithConfig(t *testing.T) { + ctx := context.Background() + + // Start Redis with snapshotting and verbose logging + redisContainer, err := tcredis.Run( + ctx, + "redis:7-alpine", + tcredis.WithSnapshotting(10, 1), // Save after 1 key changes within 10 seconds + tcredis.WithLogLevel(tcredis.LogLevelVerbose), + ) + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) + + connStr, err := redisContainer.ConnectionString(ctx) + require.NoError(t, err) + + opt, err := redis.ParseURL(connStr) + require.NoError(t, err) + + client := redis.NewClient(opt) + defer client.Close() + + // Verify Redis is running + pong, err := client.Ping(ctx).Result() + require.NoError(t, err) + require.Equal(t, "PONG", pong) + + t.Log("Redis running with custom configuration") +} diff --git a/examples/04_multi_container_network_test.go b/examples/04_multi_container_network_test.go new file mode 100644 index 0000000..b005599 --- /dev/null +++ b/examples/04_multi_container_network_test.go @@ -0,0 +1,265 @@ +package examples_test + +import ( + "context" + "database/sql" + "io" + "testing" + "time" + + _ "github.com/lib/pq" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/exec" + "github.com/testcontainers/testcontainers-go/modules/postgres" + tcredis "github.com/testcontainers/testcontainers-go/modules/redis" + "github.com/testcontainers/testcontainers-go/network" +) + +// TestMultiContainerNetwork demonstrates connecting multiple containers on a custom network +func TestMultiContainerNetwork(t *testing.T) { + ctx := context.Background() + + // Create a custom network + nw, err := network.New(ctx) + testcontainers.CleanupNetwork(t, nw) + require.NoError(t, err) + + t.Log("Custom network created") + + // Start PostgreSQL on the network with alias "database" + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase("appdb"), + network.WithNetwork([]string{"database"}, nw), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + require.NoError(t, err) + + t.Log("PostgreSQL started with network alias 'database'") + + // Start Redis on the same network with alias "cache" + redisContainer, err := tcredis.Run( + ctx, + "redis:7-alpine", + network.WithNetwork([]string{"cache"}, nw), + ) + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) + + t.Log("Redis started with network alias 'cache'") + + // Connect to PostgreSQL from host + pgConnStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("postgres", pgConnStr) + require.NoError(t, err) + defer db.Close() + + err = db.Ping() + require.NoError(t, err) + + t.Log("Successfully connected to PostgreSQL from host") + + // Connect to Redis from host + redisConnStr, err := redisContainer.ConnectionString(ctx) + require.NoError(t, err) + + redisOpt, err := redis.ParseURL(redisConnStr) + require.NoError(t, err) + + redisClient := redis.NewClient(redisOpt) + defer redisClient.Close() + + pong, err := redisClient.Ping(ctx).Result() + require.NoError(t, err) + require.Equal(t, "PONG", pong) + + t.Log("Successfully connected to Redis from host") + + // Demonstrate that containers can resolve each other by alias + // We'll use exec to ping from postgres to redis (if tools were available) + // In a real scenario, your application container would connect using these aliases + + t.Log("Both containers are on the same network and can communicate") + t.Log("An application container could connect to:") + t.Log(" - PostgreSQL at: database:5432") + t.Log(" - Redis at: cache:6379") +} + +// TestApplicationWithDependencies simulates an application container that depends on database and cache +func TestApplicationWithDependencies(t *testing.T) { + ctx := context.Background() + + // Create network + nw, err := network.New(ctx) + testcontainers.CleanupNetwork(t, nw) + require.NoError(t, err) + + // Start PostgreSQL + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase("myapp"), + postgres.WithUsername("appuser"), + postgres.WithPassword("apppass"), + network.WithNetwork([]string{"postgres"}, nw), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, pgContainer) + require.NoError(t, err) + + // Start Redis + redisContainer, err := tcredis.Run( + ctx, + "redis:7-alpine", + network.WithNetwork([]string{"redis"}, nw), + ) + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) + + // In a real test, you would start your application container here with environment variables: + // appContainer, err := testcontainers.Run( + // ctx, + // "myapp:latest", + // testcontainers.WithEnv(map[string]string{ + // "DATABASE_URL": "postgres://appuser:apppass@postgres:5432/myapp?sslmode=disable", + // "REDIS_URL": "redis://redis:6379", + // }), + // network.WithNetwork([]string{"app"}, nw), + // testcontainers.WithWaitStrategy( + // wait.ForHTTP("/health").WithPort("8080/tcp"), + // ), + // ) + + // For this example, we'll verify the dependencies are ready + pgConnStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("postgres", pgConnStr) + require.NoError(t, err) + defer db.Close() + + err = db.Ping() + require.NoError(t, err) + + redisConnStr, err := redisContainer.ConnectionString(ctx) + require.NoError(t, err) + + redisOpt, err := redis.ParseURL(redisConnStr) + require.NoError(t, err) + + redisClient := redis.NewClient(redisOpt) + defer redisClient.Close() + + _, err = redisClient.Ping(ctx).Result() + require.NoError(t, err) + + t.Log("All dependencies are ready for application container") +} + +// TestContainerCommunication demonstrates how to verify containers can communicate +func TestContainerCommunication(t *testing.T) { + ctx := context.Background() + + // Create network + nw, err := network.New(ctx) + testcontainers.CleanupNetwork(t, nw) + require.NoError(t, err) + + // Start two alpine containers for testing communication + alpine1, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithCmd("sleep", "300"), + network.WithNetwork([]string{"host1"}, nw), + ) + testcontainers.CleanupContainer(t, alpine1) + require.NoError(t, err) + + alpine2, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithCmd("sleep", "300"), + network.WithNetwork([]string{"host2"}, nw), + ) + testcontainers.CleanupContainer(t, alpine2) + require.NoError(t, err) + + // Test connectivity by pinging host2 (ping is available in alpine by default) + exitCode, reader, err := alpine1.Exec(ctx, []string{"ping", "-c", "1", "host2"}, exec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, exitCode, "Should be able to ping host2 from host1") + + // Read output to verify ping succeeded + output, err := io.ReadAll(reader) + require.NoError(t, err) + require.Contains(t, string(output), "1 packets transmitted, 1 packets received") + + t.Log("Containers can successfully communicate over custom network") +} + +// TestWaitForMultipleContainers demonstrates starting multiple containers and waiting for all to be ready +func TestWaitForMultipleContainers(t *testing.T) { + ctx := context.Background() + + nw, err := network.New(ctx) + testcontainers.CleanupNetwork(t, nw) + require.NoError(t, err) + + // Start containers concurrently (they'll wait for their respective services) + type containerResult struct { + name string + err error + } + + results := make(chan containerResult, 2) + + // Start PostgreSQL + go func() { + pgContainer, err := postgres.Run( + ctx, + "postgres:16-alpine", + network.WithNetwork([]string{"db"}, nw), + postgres.BasicWaitStrategies(), + ) + if err == nil { + testcontainers.CleanupContainer(t, pgContainer) + } + results <- containerResult{name: "postgres", err: err} + }() + + // Start Redis + go func() { + redisContainer, err := tcredis.Run( + ctx, + "redis:7-alpine", + network.WithNetwork([]string{"cache"}, nw), + ) + if err == nil { + testcontainers.CleanupContainer(t, redisContainer) + } + results <- containerResult{name: "redis", err: err} + }() + + // Wait for both to be ready + timeout := time.After(60 * time.Second) + readyCount := 0 + + for readyCount < 2 { + select { + case result := <-results: + require.NoError(t, result.err, "Failed to start %s", result.name) + t.Logf("%s is ready", result.name) + readyCount++ + case <-timeout: + t.Fatal("Timeout waiting for containers to be ready") + } + } + + t.Log("All containers started successfully") +} diff --git a/examples/05_generic_container_test.go b/examples/05_generic_container_test.go new file mode 100644 index 0000000..e7aa6ee --- /dev/null +++ b/examples/05_generic_container_test.go @@ -0,0 +1,382 @@ +package examples_test + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/exec" + "github.com/testcontainers/testcontainers-go/wait" +) + +// TestGenericNginx demonstrates using a generic container with nginx +func TestGenericNginx(t *testing.T) { + ctx := context.Background() + + // Start nginx container + nginxContainer, err := testcontainers.Run( + ctx, + "nginx:alpine", + testcontainers.WithExposedPorts("80/tcp"), + testcontainers.WithWaitStrategy( + wait.ForListeningPort("80/tcp").WithStartupTimeout(30*time.Second), + ), + ) + testcontainers.CleanupContainer(t, nginxContainer) + require.NoError(t, err) + + // Get endpoint + endpoint, err := nginxContainer.Endpoint(ctx, "http") + require.NoError(t, err) + + // Test the nginx default page + resp, err := http.Get(endpoint) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Welcome to nginx") + + t.Log("Successfully accessed nginx container") +} + +// TestGenericContainerWithCustomHTML demonstrates serving custom content with nginx +func TestGenericContainerWithCustomHTML(t *testing.T) { + ctx := context.Background() + + customHTML := ` + +Test Page +

Hello from Testcontainers!

+` + + // Start nginx with custom HTML + nginxContainer, err := testcontainers.Run( + ctx, + "nginx:alpine", + testcontainers.WithExposedPorts("80/tcp"), + testcontainers.WithFiles(testcontainers.ContainerFile{ + Reader: strings.NewReader(customHTML), + ContainerFilePath: "/usr/share/nginx/html/index.html", + FileMode: 0o644, + }), + testcontainers.WithWaitStrategy( + wait.ForListeningPort("80/tcp"), + ), + ) + testcontainers.CleanupContainer(t, nginxContainer) + require.NoError(t, err) + + endpoint, err := nginxContainer.Endpoint(ctx, "http") + require.NoError(t, err) + + resp, err := http.Get(endpoint) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Hello from Testcontainers!") + + t.Log("Successfully served custom HTML from nginx") +} + +// TestGenericContainerWithEnv demonstrates using environment variables +func TestGenericContainerWithEnv(t *testing.T) { + ctx := context.Background() + + // Start alpine container that echoes an environment variable + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithEnv(map[string]string{ + "MY_VAR": "test_value", + "ANOTHER_VAR": "another_value", + }), + testcontainers.WithCmd("sleep", "300"), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // Execute command to read environment variable + exitCode, reader, err := alpineContainer.Exec(ctx, []string{"sh", "-c", "echo $MY_VAR"}, exec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, exitCode) + + output, err := io.ReadAll(reader) + require.NoError(t, err) + require.Contains(t, string(output), "test_value") + + t.Log("Successfully used environment variables in container") +} + +// TestGenericContainerWithCommand demonstrates running a custom command +func TestGenericContainerWithCommand(t *testing.T) { + ctx := context.Background() + + // Start alpine with a custom command that creates a file + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithCmd("sh", "-c", "echo 'Hello' > /tmp/hello.txt && sleep 300"), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // Give it a moment to create the file + time.Sleep(1 * time.Second) + + // Read the file we created + exitCode, reader, err := alpineContainer.Exec(ctx, []string{"cat", "/tmp/hello.txt"}, exec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, exitCode) + + output, err := io.ReadAll(reader) + require.NoError(t, err) + require.Contains(t, string(output), "Hello") + + t.Log("Successfully ran custom command in container") +} + +// TestGenericContainerWithLabels demonstrates using labels +func TestGenericContainerWithLabels(t *testing.T) { + ctx := context.Background() + + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithLabels(map[string]string{ + "app": "testapp", + "environment": "test", + "version": "1.0", + }), + testcontainers.WithCmd("sleep", "300"), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // Inspect container to verify labels + inspect, err := alpineContainer.Inspect(ctx) + require.NoError(t, err) + + require.Equal(t, "testapp", inspect.Config.Labels["app"]) + require.Equal(t, "test", inspect.Config.Labels["environment"]) + require.Equal(t, "1.0", inspect.Config.Labels["version"]) + + t.Log("Successfully set and verified container labels") +} + +// TestGenericContainerWithTmpfs demonstrates using temporary filesystems +func TestGenericContainerWithTmpfs(t *testing.T) { + ctx := context.Background() + + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithTmpfs(map[string]string{ + "/tmp": "rw,size=100m", + }), + testcontainers.WithCmd("sleep", "300"), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // Verify tmpfs is mounted + exitCode, reader, err := alpineContainer.Exec(ctx, []string{"mount"}, exec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, exitCode) + + output, err := io.ReadAll(reader) + require.NoError(t, err) + require.Contains(t, string(output), "tmpfs on /tmp") + + t.Log("Successfully mounted tmpfs in container") +} + +// TestGenericContainerLogs demonstrates accessing container logs +func TestGenericContainerLogs(t *testing.T) { + ctx := context.Background() + + // Start container that produces logs + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithCmd("sh", "-c", "echo 'Starting...'; sleep 1; echo 'Running...'; sleep 300"), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // Wait a moment for logs to be written + time.Sleep(2 * time.Second) + + // Read logs + logs, err := alpineContainer.Logs(ctx) + require.NoError(t, err) + defer logs.Close() + + logContent, err := io.ReadAll(logs) + require.NoError(t, err) + + logStr := string(logContent) + require.Contains(t, logStr, "Starting...") + require.Contains(t, logStr, "Running...") + + t.Log("Successfully read container logs") +} + +// TestGenericContainerExec demonstrates executing commands in a running container +func TestGenericContainerExec(t *testing.T) { + ctx := context.Background() + + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithCmd("sleep", "300"), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // Execute multiple commands + tests := []struct { + name string + cmd []string + expected string + }{ + { + name: "echo", + cmd: []string{"echo", "hello world"}, + expected: "hello world", + }, + { + name: "pwd", + cmd: []string{"pwd"}, + expected: "/", + }, + { + name: "uname", + cmd: []string{"uname", "-s"}, + expected: "Linux", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exitCode, reader, err := alpineContainer.Exec(ctx, tt.cmd, exec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, exitCode) + + output, err := io.ReadAll(reader) + require.NoError(t, err) + require.Contains(t, string(output), tt.expected) + }) + } + + t.Log("Successfully executed multiple commands") +} + +// TestGenericContainerHTTPWait demonstrates waiting for an HTTP endpoint +func TestGenericContainerHTTPWait(t *testing.T) { + ctx := context.Background() + + nginxContainer, err := testcontainers.Run( + ctx, + "nginx:alpine", + testcontainers.WithExposedPorts("80/tcp"), + testcontainers.WithWaitStrategy( + wait.ForListeningPort("80/tcp"), + wait.ForHTTP("/"). + WithPort("80/tcp"). + WithStatusCodeMatcher(func(status int) bool { + return status == http.StatusOK + }). + WithStartupTimeout(30*time.Second), + ), + ) + testcontainers.CleanupContainer(t, nginxContainer) + require.NoError(t, err) + + endpoint, err := nginxContainer.Endpoint(ctx, "http") + require.NoError(t, err) + + // Container is already ready because wait strategy succeeded + resp, err := http.Get(endpoint) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + t.Log("HTTP wait strategy worked correctly") +} + +// TestGenericContainerLogWait demonstrates waiting for a log message +func TestGenericContainerLogWait(t *testing.T) { + ctx := context.Background() + + alpineContainer, err := testcontainers.Run( + ctx, + "alpine:latest", + testcontainers.WithCmd( + "sh", "-c", + "echo 'Initializing...'; sleep 2; echo 'Ready!'; sleep 300", + ), + testcontainers.WithWaitStrategy( + wait.ForLog("Ready!").WithStartupTimeout(10*time.Second), + ), + ) + testcontainers.CleanupContainer(t, alpineContainer) + require.NoError(t, err) + + // If we got here, the "Ready!" message was logged + t.Log("Container became ready after logging expected message") +} + +// TestGenericContainerPortInfo demonstrates getting port information +func TestGenericContainerPortInfo(t *testing.T) { + ctx := context.Background() + + nginxContainer, err := testcontainers.Run( + ctx, + "nginx:alpine", + testcontainers.WithExposedPorts("80/tcp", "443/tcp"), + testcontainers.WithWaitStrategy(wait.ForListeningPort("80/tcp")), + ) + testcontainers.CleanupContainer(t, nginxContainer) + require.NoError(t, err) + + // Method 1: Get mapped port + port80, err := nginxContainer.MappedPort(ctx, "80/tcp") + require.NoError(t, err) + t.Logf("Port 80 is mapped to: %s", port80.Port()) + + // Method 2: Get host + host, err := nginxContainer.Host(ctx) + require.NoError(t, err) + t.Logf("Container host: %s", host) + + // Method 3: Get endpoint + endpoint, err := nginxContainer.Endpoint(ctx, "http") + require.NoError(t, err) + t.Logf("HTTP endpoint: %s", endpoint) + + // Method 4: Get all ports + ports, err := nginxContainer.Ports(ctx) + require.NoError(t, err) + t.Logf("All ports: %v", ports) + + // Verify we can access port 80 + resp, err := http.Get(fmt.Sprintf("http://%s:%s", host, port80.Port())) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + t.Log("Successfully retrieved and used port information") +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..b7225c4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,263 @@ +# Testcontainers for Go Examples + +This directory contains practical, runnable examples demonstrating various features and patterns of Testcontainers for Go. + +## Prerequisites + +Before running these examples, you need: + +1. **Go 1.24+** installed +2. **Docker** running locally +3. Required Go packages: + +```bash +go get github.com/testcontainers/testcontainers-go +go get github.com/testcontainers/testcontainers-go/modules/postgres +go get github.com/testcontainers/testcontainers-go/modules/redis +go get github.com/stretchr/testify/require +go get github.com/lib/pq +go get github.com/redis/go-redis/v9 +``` + +## Examples Overview + +### 01_postgres_basic_test.go +**Basic PostgreSQL Usage** + +Demonstrates: +- Starting a PostgreSQL container with default settings +- Connecting to PostgreSQL +- Custom database configuration (database name, username, password) +- Creating schemas and inserting data + +Run with: +```bash +go test -v -run TestBasicPostgres +go test -v -run TestPostgresWithCustomConfig +go test -v -run TestPostgresWithSchema +``` + +### 02_postgres_snapshot_test.go +**PostgreSQL Snapshots for Test Isolation** + +Demonstrates: +- Creating database snapshots +- Modifying data and restoring to previous state +- Using multiple named snapshots + +This is extremely useful for: +- Running multiple tests against the same initial state +- Test isolation without restarting containers +- Fast test execution + +Run with: +```bash +go test -v -run TestPostgresSnapshot +go test -v -run TestPostgresMultipleSnapshots +``` + +### 03_redis_cache_test.go +**Redis Operations** + +Demonstrates: +- Basic Redis key-value operations +- Key expiration +- List operations (RPUSH, LPOP, LRANGE) +- Hash operations (HSET, HGET, HGETALL) +- Custom Redis configuration (snapshotting, log levels) + +Run with: +```bash +go test -v -run TestBasicRedis +go test -v -run TestRedisWithExpiration +go test -v -run TestRedisListOperations +go test -v -run TestRedisHashOperations +go test -v -run TestRedisWithConfig +``` + +### 04_multi_container_network_test.go +**Multi-Container Networking** + +Demonstrates: +- Creating custom Docker networks +- Connecting multiple containers on the same network +- Container-to-container communication using network aliases +- Simulating microservices architectures +- Waiting for multiple containers concurrently + +This is essential for: +- Integration testing with multiple services +- Testing service dependencies +- Simulating production-like environments + +Run with: +```bash +go test -v -run TestMultiContainerNetwork +go test -v -run TestApplicationWithDependencies +go test -v -run TestContainerCommunication +go test -v -run TestWaitForMultipleContainers +``` + +### 05_generic_container_test.go +**Generic Container Patterns** + +Demonstrates: +- Using containers without pre-configured modules +- Custom HTML content with nginx +- Environment variables +- Custom commands +- Container labels +- Temporary filesystems (tmpfs) +- Reading container logs +- Executing commands in running containers +- Different wait strategies (HTTP, log-based) +- Getting port information + +Run with: +```bash +go test -v -run TestGenericNginx +go test -v -run TestGenericContainerWithCustomHTML +go test -v -run TestGenericContainerWithEnv +go test -v -run TestGenericContainerExec +# ... and many more +``` + +## Running All Examples + +To run all examples: + +```bash +# Run all tests +go test -v ./examples/ + +# Run all tests with more details +go test -v -count=1 ./examples/ + +# Run a specific example file +go test -v ./examples/01_postgres_basic_test.go +``` + +## Common Patterns + +### 1. Basic Pattern (with Module) + +```go +ctx := context.Background() + +// Start container +pgContainer, err := postgres.Run(ctx, "postgres:16-alpine") +testcontainers.CleanupContainer(t, pgContainer) // BEFORE error check! +require.NoError(t, err) + +// Get connection details +connStr, err := pgContainer.ConnectionString(ctx) +require.NoError(t, err) + +// Use the container... +``` + +### 2. Generic Container Pattern + +```go +ctx := context.Background() + +ctr, err := testcontainers.Run( + ctx, + "image:tag", + testcontainers.WithExposedPorts("8080/tcp"), + testcontainers.WithEnv(map[string]string{"KEY": "value"}), + testcontainers.WithWaitStrategy(wait.ForListeningPort("8080/tcp")), +) +testcontainers.CleanupContainer(t, ctr) +require.NoError(t, err) +``` + +### 3. Multi-Container Pattern + +```go +ctx := context.Background() + +// Create network +nw, err := network.New(ctx) +testcontainers.CleanupNetwork(t, nw) +require.NoError(t, err) + +// Start containers on network +db, err := postgres.Run(ctx, "postgres:16-alpine", + network.WithNetwork([]string{"database"}, nw)) +testcontainers.CleanupContainer(t, db) + +app, err := testcontainers.Run(ctx, "myapp:latest", + network.WithNetwork([]string{"app"}, nw)) +testcontainers.CleanupContainer(t, app) +``` + +## Tips and Best Practices + +1. **Always register cleanup before checking errors** + ```go + ctr, err := testcontainers.Run(ctx, "image") + testcontainers.CleanupContainer(t, ctr) // Call this first! + require.NoError(t, err) // Then check error + ``` + +2. **Use pre-configured modules when available** + - Modules provide sensible defaults + - Helper methods like `ConnectionString()` + - Automatic credential management + +3. **Use snapshots for test isolation** + - Much faster than restarting containers + - Perfect for test suites with shared setup + +4. **Use custom networks for multi-container tests** + - Containers can communicate via aliases + - More realistic than host networking + +5. **Use appropriate wait strategies** + - `ForListeningPort` - when service listens on a port + - `ForLog` - when service logs a ready message + - `ForHTTP` - when service has an HTTP health endpoint + +## Troubleshooting + +### Container won't start +- Check if Docker is running: `docker ps` +- Check Docker logs: add `testcontainers.WithLogConsumers(&testcontainers.StdoutLogConsumer{})` +- Increase timeout: `wait.ForListeningPort("80/tcp").WithStartupTimeout(60*time.Second)` + +### Port conflicts +- Testcontainers auto-assigns random ports +- Don't manually specify host ports + +### Image pull failures +- Pull manually first: `docker pull postgres:16-alpine` +- Check network connectivity +- For private registries: `docker login registry.example.com` + +### Cleanup issues +- Verify Ryuk is running: `docker ps | grep ryuk` +- Check cleanup order: network cleanup after container cleanup +- Enable Ryuk logging: `export RYUK_VERBOSE=true` + +## Additional Resources + +- [Testcontainers for Go Documentation](https://golang.testcontainers.org/) +- [Available Modules](https://golang.testcontainers.org/modules/) +- [GitHub Repository](https://github.com/testcontainers/testcontainers-go) + +## Module Dependencies + +To run specific examples, you may need additional module dependencies: + +```bash +# For PostgreSQL examples +go get github.com/lib/pq +go get github.com/testcontainers/testcontainers-go/modules/postgres + +# For Redis examples +go get github.com/redis/go-redis/v9 +go get github.com/testcontainers/testcontainers-go/modules/redis + +# Note: network is part of the main testcontainers-go module, not a separate module +``` diff --git a/examples/go.mod b/examples/go.mod new file mode 100644 index 0000000..5c0db81 --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,75 @@ +module github.com/testcontainers/testcontainers-go/examples + +go 1.24.0 + +toolchain go1.24.7 + +require ( + github.com/lib/pq v1.10.9 + github.com/redis/go-redis/v9 v9.7.3 + github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.39.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 + github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mdelapenya/tlscert v0.2.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/go.sum b/examples/go.sum new file mode 100644 index 0000000..43a9292 --- /dev/null +++ b/examples/go.sum @@ -0,0 +1,220 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= +github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= +github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 h1:REJz+XwNpGC/dCgTfYvM4SKqobNqDBfvhq74s2oHTUM= +github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0/go.mod h1:4K2OhtHEeT+JSIFX4V8DkGKsyLa96Y2vLdd3xsxD5HE= +github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= +github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..c51f536 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,81 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:testcontainers/claude-skills:testcontainers-go", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "2032c49f293a8f3615bad72afd6b86ce81eff615", + "treeHash": "d18459abf7a17e71524d86043a4b1528aee665caeb5747bf2c269065f7cfb4ac", + "generatedAt": "2025-11-28T10:28:39.295819Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "testcontainers-go", + "description": "A comprehensive guide for using Testcontainers for Go to write reliable integration tests with Docker containers in Go projects. Supports 62+ pre-configured modules for databases, message queues, cloud services, and more.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "LICENSE", + "sha256": "d123f6c77b4c4dcf22191360afab21e10c349463efd7453af0f6486b65487c82" + }, + { + "path": "README.md", + "sha256": "2891c81eb565669faa454f44b81e3ca0a34079b3d77aeb936c6b777abb991dcb" + }, + { + "path": "SKILL.md", + "sha256": "e1f3a53284118c865ea6c72be2d1b9bca3d6882188a802f7dc9df105174d4799" + }, + { + "path": "examples/02_postgres_snapshot_test.go", + "sha256": "01a88b6387f1dfe0c7b462c8eec6711782453bd849d31d838c3d08faa1af0eb5" + }, + { + "path": "examples/go.mod", + "sha256": "d8e4deaad2abca28ff7f142c19a877f139b1c1acf16a89e6adcfcff1a4c06073" + }, + { + "path": "examples/03_redis_cache_test.go", + "sha256": "df8936199084492e7b78f402c117f990e96a40b90d3891f2f75c3e1c634b0bc2" + }, + { + "path": "examples/05_generic_container_test.go", + "sha256": "2adcadfa4a13f039150b7f62547c3a7c38e35ce71344413090cb0d94b4c9a268" + }, + { + "path": "examples/04_multi_container_network_test.go", + "sha256": "a0ba9896da71694b1e4304a43199b84e72bbb42aa2eb45ed296515d946d33388" + }, + { + "path": "examples/go.sum", + "sha256": "c71b606d469e904cd8a723e9540bc34dd058f6f80d75f64848b8e41de7aaf837" + }, + { + "path": "examples/01_postgres_basic_test.go", + "sha256": "1065e9468a2fe5377fc9f3ad876ddff1b9d593bbc0afb586532c44b0311d90c4" + }, + { + "path": "examples/README.md", + "sha256": "20916b15f75cce1b1d6f87837403589758039d54db510012285fd9cee6c6e48b" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "51cc5eeb826fbe668541ce844bfd5e6f86cdfd8a2059ad04519deca4790f6ca4" + } + ], + "dirSha256": "d18459abf7a17e71524d86043a4b1528aee665caeb5747bf2c269065f7cfb4ac" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file