mcpField
T
testcontainers-go

testcontainers-go

github.com/freightcognition测试与质量447 次安装2 周前更新
代码仓库

关于 testcontainers-go

testcontainers-go 是面向 Go 开发者的集成测试指南,帮助使用 Docker 容器中的真实服务编写测试。它涵盖预配置模块、通用容器、网络配置、等待策略和资源清理。

主要功能

  1. 01使用预配置服务模块
  2. 02在测试中启动和管理容器
  3. 03配置网络、卷和环境变量
  4. 04设置等待策略以确认服务就绪
  5. 05管理容器资源清理

适用场景

  1. 01使用真实数据库或消息队列测试
  2. 02对多个依赖版本运行测试
  3. 03创建可复现的测试环境
  4. 04在集成测试中替代外部依赖模拟

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/testcontainers-go确认已加载。

详细介绍

使用 Testcontainers for Go 进行集成测试

全面介绍如何在 Go 项目中使用 Testcontainers for Go 和 Docker 容器编写可靠的集成测试。

描述

此技能可帮助你使用 Testcontainers for Go 编写集成测试。Testcontainers for Go 是一个 Go 库,可提供常见数据库、消息队列、Web 浏览器或任何可在 Docker 容器中运行的程序的轻量级临时实例。

主要功能:

  • 使用 62+ 个预配置模块,支持常见服务(数据库、消息队列、云服务等)
  • 在 Go 测试中设置和管理 Docker 容器
  • 配置网络、卷和环境变量
  • 实施适当的清理和资源管理
  • 调试和排查容器问题

何时使用此技能

在以下情况下使用此技能:

  • 编写需要真实服务(数据库、消息队列等)的集成测试
  • 针对依赖项的多个版本或配置进行测试
  • 创建可重现的测试环境
  • 在集成测试中避免模拟外部依赖项
  • 设置临时测试基础设施

先决条件

  • 已安装并运行 Docker 或 Podman
  • Go 1.24+(查看 go.mod 了解项目的具体要求)
  • 可访问 Docker socket,位于标准路径(macOS/Windows 上的 Docker Desktop,Linux 上的 /var/run/docker.sock)

说明

1. 安装和设置

将 testcontainers-go 添加到项目中:

go get github.com/testcontainers/testcontainers-go

对于预配置模块(推荐):

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

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

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

验证 Docker 是否可用:

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

2. 使用预配置模块(推荐方法)

Testcontainers for Go 提供 62+ 个预配置模块,这些模块提供适用于生产环境的配置、合理的默认值和辅助方法。如果有可用模块,务必优先使用模块,而非通用容器。

为什么使用模块?
  • 合理的默认值:预配置端口、环境变量和等待策略
  • 连接辅助方法:内置 ConnectionString()、Endpoint() 等方法
  • 专用功能:模块专属功能(例如,Postgres 快照、Kafka 主题管理)
  • 自动凭据:安全的凭据生成和管理
  • 经过实战验证:数千个项目已在生产环境中使用
可用模块类别

数据库(17 个模块):

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

消息队列(6 个模块):

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

搜索与向量数据库(9 个模块):

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

云与基础设施(6 个模块):

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

服务与工具(13 个模块):

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

开发(10 个模块):

  • compose、registry、k6、ollama、grafana-lgtm
  • dockermodelrunner、dockermcpgateway、socat、mssql
基本模块使用模式
package myapp_test

import (
    "context"
    "testing"

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

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

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

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

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

    // Run your tests...
}
使用选项配置模块

模块支持三个级别的自定义:

级别 1:简单选项(通过 testcontainers.CustomizeRequestOption)

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

级别 2:模块专属选项

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

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

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

级别 3:通过生命周期钩子进行高级配置

// PostgreSQL with custom initialization
pgContainer, err := postgres.Run(
    ctx,
    "postgres:16-alpine",
    postgres.WithDatabase("myapp"),
    testcontainers.WithLifecycleHooks(
        testcontainers.ContainerLifecycleHooks{
            PostStarts: []testcontainers.ContainerHook{
                func(ctx context.Context, c testcontainers.Container) error {
                    // Custom initialization after container starts
                    return nil
                },
            },
        },
    ),
)
模块专属辅助方法

大多数模块除了提供 ConnectionString() 外,还提供便捷方法:

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

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

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

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

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

    // Make changes
    db.Exec("INSERT INTO users (name) VALUES ('Bob')")

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

    // Bob is gone, only Alice remains
}

// Kafka: Get bootstrap servers
kafkaContainer, _ := kafka.Run(ctx, "confluentinc/confluent-local:7.5.0")
brokers, _ := kafkaContainer.Brokers(ctx)
查找合适的模块
  1. 浏览可用模块:https://testcontainers.com/modules/?language=go(完整且最新的列表)
  2. 查看模块目录:/modules/,位于 testcontainers-go GitHub 仓库
  3. 模块文档:https://golang.testcontainers.org/modules/(各模块的在线文档)
  4. 按类别浏览(见上方列表)
  5. 搜索示例:每个模块的目录中都有 examples_test.go

模块路径模式:

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

3. 使用通用容器(备用方案)

如果没有预配置的模块,请使用通用容器。

重要:暴露端口时务必添加等待策略,以确保容器在测试运行前已准备就绪。这对可靠性至关重要,在 CI 环境中尤其如此。切勿用 time.Sleep 替代等待策略——这是反模式,会导致测试时好时坏。

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

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

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

常见的通用容器选项:

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

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

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

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

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

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

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

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

4. 编写集成测试
测试结构最佳实践
package myapp_test

import (
    "context"
    "testing"

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

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

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

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

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

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

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

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

    // Test your application logic here...
}

关键模式:在检查错误之前进行清理

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

// WRONG: Creates resource leaks
ctr, err := testcontainers.Run(ctx, "nginx:alpine")
require.NoError(t, err)                   // If this fails...
testcontainers.CleanupContainer(t, ctr)  // ...cleanup never registers
使用容器的表驱动测试
func TestMultipleVersions(t *testing.T) {
    ctx := context.Background()

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

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

            // Run tests against this version...
        })
    }
}
并行执行测试
func TestParallelContainers(t *testing.T) {
    t.Parallel()  // Enable parallel execution

    ctx := context.Background()

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

    // Each parallel test gets its own container
}

5. 容器网络
连接多个容器
import "github.com/testcontainers/testcontainers-go/network"

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

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

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

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

    // Test application can communicate with database...
}
访问容器端口
func TestPortAccess(t *testing.T) {
    ctx := context.Background()

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

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

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

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

6. 资源管理与清理
清理方法

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

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

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

    // Container automatically cleaned up when test ends
}

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

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

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

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

方法 3:defer(旧式)

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

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

    defer func() {
        err := testcontainers.TerminateContainer(ctr)
        require.NoError(t, err)
    }()
}
清理选项
// Cleanup with custom timeout
testcontainers.CleanupContainer(t, ctr,
    testcontainers.StopTimeout(10*time.Second),
)

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

// Combine options
testcontainers.CleanupContainer(t, ctr,
    testcontainers.StopTimeout(5*time.Second),
    testcontainers.RemoveVolumes("data"),
)
使用 Ryuk 自动清理

Go 版 Testcontainers 使用 Ryuk,这是一个垃圾回收器,即使测试崩溃或超时,也会自动清理容器:

  • 以边车容器的形式运行(testcontainers/ryuk:0.13.0)
  • 监控测试会话的生命周期
  • 在会话结束时清理容器
  • 处理并行测试执行

控制 Ryuk 的行为:

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

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

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

7. 配置模式
环境变量
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithEnv(map[string]string{
        "DATABASE_URL": "postgres://localhost/db",
        "LOG_LEVEL":    "debug",
        "API_KEY":      "test-key",
    }),
)
在容器中执行命令

使用 Exec() 执行命令时,建议使用 exec.Multiplexed(),以正确处理 Docker 的输出格式:

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

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

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

为什么使用 exec.Multiplexed()?

  • 从输出中移除 Docker 的多路复用标头
  • 将 stdout 和 stderr 合并为一个干净的流
  • 让输出更易于阅读和解析

不使用 exec.Multiplexed() 时,得到的将是 Docker 原始的多路复用流,其中包含难以解析的标头字节。

文件和目录
// Copy single file
testcontainers.Run(
    ctx,
    "nginx:alpine",
    testcontainers.WithFiles(testcontainers.ContainerFile{
        Reader:            strings.NewReader("server { listen 80; }"),
        ContainerFilePath: "/etc/nginx/conf.d/default.conf",
        FileMode:          0o644,
    }),
)

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

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

        // Named volume
        hc.Mounts = []mount.Mount{
            {
                Type:   mount.TypeVolume,
                Source: "pgdata",
                Target: "/var/lib/postgresql/data",
            },
        }
    }),
)
临时文件系统
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithTmpfs(map[string]string{
        "/tmp":      "rw",
        "/app/temp": "rw,size=100m,mode=1777",
    }),
)

8. 等待策略

等待策略对测试可靠性至关重要。 它们可确保容器在测试运行前完全准备就绪,这在时序可能变化的 CI 环境中尤为重要。

最佳实践:

  • ✅ 暴露端口时务必使用 wait.ForListeningPort()——这是最可靠的做法
  • ✅ 根据服务选择合适的等待策略(HTTP 健康检查、日志模式等)
  • ❌ 切勿使用 time.Sleep()——这是反模式,会导致测试时好时坏
  • ✅ 设置合理的超时时间,以应对运行缓慢的 CI 环境
基于端口的等待(暴露端口时推荐)
import "github.com/testcontainers/testcontainers-go/wait"

testcontainers.Run(
    ctx,
    "postgres:16",
    testcontainers.WithWaitStrategy(
        wait.ForListeningPort("5432/tcp").
            WithStartupTimeout(30*time.Second).
            WithPollInterval(1*time.Second),
    ),
)
基于日志的等待
testcontainers.Run(
    ctx,
    "elasticsearch:8.7.0",
    testcontainers.WithWaitStrategy(
        wait.ForLog("started").
            WithStartupTimeout(60*time.Second).
            WithOccurrence(1),
    ),
)
基于 HTTP 的等待
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithWaitStrategy(
        wait.ForHTTP("/health").
            WithPort("8080/tcp").
            WithStatusCodeMatcher(func(status int) bool {
                return status == 200
            }).
            WithStartupTimeout(30*time.Second),
    ),
)
基于 SQL 的等待
testcontainers.Run(
    ctx,
    "postgres:16",
    testcontainers.WithWaitStrategy(
        wait.ForSQL("5432/tcp", "postgres", func(host string, port nat.Port) string {
            return fmt.Sprintf("postgres://user:pass@%s:%s/db?sslmode=disable",
                host, port.Port())
        }).WithStartupTimeout(30*time.Second),
    ),
)
多种等待策略
testcontainers.Run(
    ctx,
    "myapp:latest",
    testcontainers.WithWaitStrategy(
        wait.ForAll(
            wait.ForListeningPort("8080/tcp"),
            wait.ForLog("Application started"),
            wait.ForHTTP("/health"),
        ),
    ),
)

9. 故障排除
检查 Docker 是否可用
func TestDockerConnection(t *testing.T) {
    testcontainers.SkipIfProviderIsNotHealthy(t)

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

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

    t.Logf("Docker version: %s", info.ServerVersion)
    t.Logf("OS: %s", info.OperatingSystem)
}
调试容器日志
func TestWithLogging(t *testing.T) {
    ctx := context.Background()

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

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

    // Method 3: Inspect container
    info, _ := ctr.Inspect(ctx)
    t.Logf("Container state: %+v", info.State)
}
常见问题

问题:容器启动超时

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

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

问题:端口已被占用

  • Testcontainers 会自动分配随机端口
  • 除非必要,否则不要手动指定主机端口
  • 检查是否有未清理的容器:docker ps -a

问题:镜像拉取失败

# Pull manually first to verify
docker pull postgres:16

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

问题:容器未清理

// Verify Ryuk is running
docker ps | grep ryuk

// Check cleanup is registered correctly
testcontainers.CleanupContainer(t, ctr)  // Before error check!
用于调试的环境变量
# Enable Ryuk verbose logging
export RYUK_VERBOSE=true

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

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

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

示例

示例 1:PostgreSQL 集成测试
package myapp_test

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

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

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

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

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

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

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

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

    t.Run("GetUser", func(t *testing.T) {
        user, err := repo.GetByEmail("[email protected]")
        require.NoError(t, err)
        require.Equal(t, "Alice", user.Name)
    })
}
示例 2:Redis 缓存测试
package cache_test

import (
    "context"
    "testing"
    "time"

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

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

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

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

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

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

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

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

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

        time.Sleep(2 * time.Second)

        _, err = client.Get(ctx, "key2").Result()
        require.Equal(t, redis.Nil, err)
    })
}
示例 3:Kafka 生产者/消费者测试
package messaging_test

import (
    "context"
    "testing"
    "time"

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

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

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

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

    topic := "test-topic"

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

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

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

        // Consume message
        msg, err := reader.ReadMessage(ctx)
        require.NoError(t, err)
        require.Equal(t, "Hello, Kafka!", string(msg.Value))
    })
}
示例 4:多容器应用程序堆栈
package integration_test

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

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

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

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

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

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

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

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

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

import (
    "context"
    "testing"

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

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

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

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

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

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

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

    // Run tests against the stack...
}

最佳实践

  1. 始终使用可用的预配置模块 - 它们提供合理的默认值和辅助方法
  2. 立即注册清理操作 - 在检查错误之前调用 testcontainers.CleanupContainer(t, ctr)
  3. 暴露端口时始终添加等待策略 - 使用 wait.ForListeningPort() 确保可靠性,尤其是在 CI 环境中。切勿使用 time.Sleep() —— 这是一种反模式,会导致测试不稳定
  4. 选择适当的等待策略 - 对健康检查端点使用 wait.ForHTTP(),对日志模式使用 wait.ForLog(),或对端口可用性使用 wait.ForListeningPort()
  5. 利用表驱动测试 - 针对多个版本或配置进行测试
  6. 使用自定义网络 - 用于多容器通信
  7. 保持容器临时性 - 不要依赖测试之间的状态
  8. 检查 Docker 是否可用 - 使用 testcontainers.SkipIfProviderIsNotHealthy(t)
  9. 启用并行执行 - 使用 t.Parallel() 加快测试套件运行
  10. 使用模块辅助方法 - 例如,ConnectionString()、Snapshot()、Restore()
  11. 使用日志进行调试 - 排查问题时使用 WithLogConsumers()

其他资源

评论

支持 Markdown