Go Testcontainer综合指南:使用容器进行实际测试

Testcontainers for Go 通过在隔离的 Docker 容器中运行真实服务来简化集成测试。以下是您需要了解的内容:

Testcontainers for Go 通过在 Docker 容器中启用真实服务来解决此问题。这意味着您将在接近生产的条件下测试应用程序。所有交互都发生在实际数据库和消息代理中,从而提供比模拟更准确的结果。而且无需担心手动设置和拆除容器 - 该库会自动处理此问题。

主要优点:

  • 使用实际数据库、消息代理和其他服务创建隔离的测试环境
  • 消除本地和 CI 系统之间与环境相关的测试失败
  • 自动管理容器生命周期(创建和清理)

快速设置示例:

container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    ContainerRequest: testcontainers.ContainerRequest{
        Image:        "postgres:latest",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_PASSWORD": "password",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForListeningPort("5432/tcp"),
    },
    Started: true,
})

性能提示:

  • 尽可能使用 TestMain() 在测试中重用容器
  • 使用轻量级容器镜像(Alpine 变体)
  • 实施特定的就绪检查,而不是一般的等待
  • 在 Docker 设置中配置适当的资源限制
支持多种服务,包括:
  • 数据库(PostgreSQL、MongoDB)
  • 消息代理(Kafka)
  • 自定义 API 模拟
  • 多容器编排
想要可靠的集成测试?开始使用 Testcontainers 而不是模拟,以便在隔离环境中进行准确的服务行为测试。

安装和设置
要开始使用 Testcontainers for Go,您需要安装库并设置环境。这很简单,只需几个步骤。

安装 Go 的 Testcontainers
让我们为项目安装Testcontainers。打开终端并运行以下命令:
go get github.com/testcontainers/testcontainers-go

创建第一个测试
现在,让我们创建一个简单的集成测试,以验证与 PostgreSQL 数据库的交互。 在这个测试中,我们将启动一个 PostgreSQL 容器,连接到数据库并执行一个基本的 SQL 查询。 具体方法如下:打开一个测试文件,如 main_test.go,然后添加以下代码:

package main

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

    _ "github.com/lib/pq"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

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

    // Create a PostgreSQL container
    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: testcontainers.ContainerRequest{
            Image:        "postgres:latest",
            ExposedPorts: []string{"5432/tcp"},
            Env: map[string]string{
                "POSTGRES_PASSWORD": "password",
                "POSTGRES_DB":       "testdb",
            },
            WaitingFor: wait.ForListeningPort("5432/tcp"),
        },
        Started: true,
    })
    if err != nil {
        t.Fatalf("Error creating container: %s", err)
    }
    defer container.Terminate(ctx)

    // Get container information
    host, _ := container.Host(ctx)
    port, _ := container.MappedPort(ctx, "5432")

    // Connect to the PostgreSQL database inside the container
    dsn := fmt.Sprintf("postgres://postgres:password@%s:%s/testdb?sslmode=disable", host, port.Port())
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        t.Fatalf("Error connecting to the database: %s", err)
    }
    defer db.Close()

    // Perform a simple SQL query
    err = db.Ping()
    if err != nil {
        t.Fatalf("Failed to connect to the database: %s", err)
    }
}


此测试启动 PostgreSQL 容器,连接到它,并检查是否可以执行查询。测试完成后,容器将自动删除。

环境设置
为了使 Testcontainers 正常工作,必须在您的系统上安装 Docker。如果您还没有安装,请从官方网站docker.com下载并安装 Docker 。安装后,确保 Docker 正在运行,并且您可以从终端执行 Docker 命令。

此外,如果您在 macOS 或 Windows 上使用 Docker,请确保 Docker 有足够的分配内存和资源来运行容器。在 Docker 设置中,您可以配置内存、CPU 和磁盘空间的限制。

PostgreSQL 用法示例
现在我们已经安装了 Testcontainers 并设置了基础知识,让我们仔细看看上一节中的代码。在这里,我们将介绍如何创建和使用专门用于集成测试的 PostgreSQL 容器。

创建 PostgreSQL 容器
首先,我们需要创建一个用于测试的 PostgreSQL 容器。创建容器时,我们可以指定 PostgreSQL 版本、要打开的端口以及要设置的环境变量。以下是创建 PostgreSQL 容器的示例:

ctx := context.Background() 

container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 
    ContainerRequest: testcontainers.ContainerRequest{ 
        Image:         "postgres:latest" , // 指定 PostgreSQL 镜像
        ExposedPorts: [] string { "5432/tcp" }, // 开放 5432 端口用于连接
        Env: map [ string ] string { // 设置环境变量
            "POSTGRES_PASSWORD" : "password" , 
            "POSTGRES_DB" :        "testdb" , 
        }, 
        WaitingFor: wait.ForListeningPort( "5432/tcp" ), // 等待容器准备好连接
    }, 
    Started: true , // 创建后自动启动容器
}) 
if err != nil { 
    t.Fatalf( "Error creating container: %s" , err) 
}

这段代码使用 PostgreSQL 映像启动一个容器,打开 5432 端口供连接,并使用 postgres 用户的密码设置名为 testdb 的数据库。 确保容器运行就绪至关重要,因此我们使用 wait.ForListeningPort 等待端口被访问。

编写集成测试
一旦创建容器,我们就可以连接到它并开始测试与数据库的交互:

host, _ := container.Host(ctx) // 获取容器的主机
port, _ := container.MappedPort(ctx, "5432" ) // 获取映射到本地机器的端口

dsn := fmt.Sprintf( "postgres://postgres:password@%s:%s/testdb?sslmode=disable" , host, port.Port()) 
db, err := sql.Open( "postgres" , dsn) // 连接容器内的 PostgreSQL 数据库
if err != nil { 
    t.Fatalf( "连接数据库时出错:%s" , err) 

defer db.Close() // 测试结束后关闭数据库连接

// 验证我们是否可以成功连接到数据库
err = db.Ping() 
if err != nil { 
    t.Fatalf( "连接数据库失败:%s",呃)
}

在这里,我们获取容器的主机和端口信息,并用它为数据库创建连接字符串 (dsn)。 然后,我们使用 Go 的标准数据库/sql 库连接到数据库,并使用 Ping() 验证连接,以确保数据库连接正常。 该测试将检查基本功能:启动容器、成功连接到数据库以及与数据库交互。

容器拆除
测试完成后,必须正确关闭容器,以免它继续消耗系统资源。

为此,我们调用 Terminate 方法来停止并删除容器:
defer container.Terminate(ctx)

添加这一行可确保容器在测试结束后正确终止。 如果容器未被终止,它可能会继续在后台运行,从而导致资源泄漏和后续的

使用其他服务
Testcontainers 提供了一种通用方法,不仅可以应用于数据库,还可以应用于 MongoDB 和 Kafka 等其他服务。这使得测试分布式系统变得更加简单和方便。

1、MongoDB容器
MongoDB 是一种广泛使用的 NoSQL 数据库,使用 Testcontainers 设置用于测试的容器也很简单。设置过程与我们使用 PostgreSQL 的过程非常相似。让我们看看如何实现这一点:

package main 

import ( 
    "context" 
    "testing" 
    "fmt" 

    "github.com/testcontainers/testcontainers-go" 
    "github.com/testcontainers/testcontainers-go/wait" 
    "go.mongodb.org/mongo-driver/mongo" 
    "go.mongodb.org/mongo-driver/mongo/options"
 ) 

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

    // 创建一个 MongoDB 容器
    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 
        ContainerRequest: testcontainers.ContainerRequest{ 
            Image:         "mongo:latest" , // MongoDB 镜像
            ExposedPorts: [] string { "27017/tcp" }, // 打开 27017 端口进行连接
            WaitingFor: wait.ForListeningPort( "27017/tcp" ), // 等待MongoDB 已准备好接受请求
        }, 
        Started: true , 
    }) 
    if err != nil { 
        t.Fatalf( "Error creating container: %s" , err) 
    } 
    defer container.Terminate(ctx) 

    // 获取容器信息
    host, _ := container.Host(ctx) 
    port, _ := container.MappedPort(ctx, "27017" ) 

    // 使用 Mongo Driver 连接到 MongoDB
     mongoURI := fmt.Sprintf( "mongodb://%s:%s" , host, port.Port()) 
    clientOptions := options.Client().ApplyURI(mongoURI) 
    client, err := mongo.Connect(ctx, clientOptions) 
    if err != nil { 
        t.Fatalf( "Error connecting to MongoDB: %s" , err) 
    } 
    defer client.Disconnect(ctx) 

    // 验证连接是否成功
    err = client.Ping(ctx, nil ) 
    if err != nil { 
        t.Fatalf( "无法连接到 MongoDB:%s" , err) 
    } 
}

此代码启动 MongoDB 容器,使用 MongoDB 的官方 Go 驱动程序连接到数据库,并验证连接是否正常工作。与 PostgreSQL 示例一样,测试完成后容器会自动终止。

2、Kafka 容器
Kafka 是一个广泛用于数据流处理的系统,你也可以设置一个容器对其进行测试。

package main 

import ( 
    "context" 
    "testing" 

    "github.com/testcontainers/testcontainers-go" 
    "github.com/testcontainers/testcontainers-go/wait"
 ) 

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

    // 创建 Kafka 容器
    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 
        ContainerRequest: testcontainers.ContainerRequest{ 
            Image:         "confluentinc/cp-kafka:latest" , // Kafka 镜像
            ExposedPorts: [] string { "9092/tcp" }, // 打开端口 9092 以进行连接
            Env: map [ string ] string { // Kafka 的环境变量
                "KAFKA_ADVERTISED_LISTENERS" : "PLAINTEXT://localhost:9092" , 
                "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" : "PLAINTEXT:PLAINTEXT" , 
                "KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR" : "1" , 
            }, 
            WaitingFor: wait.ForListeningPort( "9092/tcp" ), // 等待 Kafka 准备就绪
        }, 
        Started: true , 
    }) 
    if err != nil { 
        t.Fatalf( "Error creating container: %s" , err) 
    } 
    defer container.Terminate(ctx) 

    // 在这里可以添加代码,通过 Kafka Producer 和 Consumer 发送和接收消息
}

在本示例中,我们启动了一个 Kafka 容器,并开放了 9092 端口用于交互。 为了保持示例的简洁性,我们没有包含发送和接收消息的逻辑,但你可以使用 sarama 或 confluent-kafka-go 等 Kafka 库轻松添加这些逻辑。


最佳实践
对于容器化测试,性能和易用性成为关键因素。使用 Testcontainers 进行测试可能比常规单元测试花费更多时间,因为每个容器都需要启动、等待准备就绪,然后才能运行测试。

如何加速测试
重复使用容器。为每个测试启动一个容器是一个资源密集型过程。但是,如果测试使用相同的基础架构,则在测试之间重复使用容器是有意义的。这可以使用全局变量或在测试包内完成,因此容器在所有测试之前启动一次,而不是为每个单独的测试启动一次。

var dbContainer testcontainers.Container 

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

    // 为所有测试启动一次容器
    dbContainer, _ = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 
        ContainerRequest: testcontainers.ContainerRequest{ 
            Image:         "postgres:latest" , 
            ExposedPorts: [] string { "5432/tcp" }, 
            Env: map [ string ] string { 
                "POSTGRES_PASSWORD" : "password" , 
                "POSTGRES_DB" :        "testdb" , 
            }, 
            WaitingFor: wait.ForListeningPort( "5432/tcp" ), 
        }, 
        Started: true , 
    }) 

    code := m.Run() // 运行所有测试

    // 所有测试结束后终止容器
    dbContainer.Terminate(ctx) 

    os.Exit(code) 
}

减少容器等待时间。启动容器时,正确配置就绪检查至关重要。尽可能使用最具体的检查。例如,如果您只需要等到端口准备好连接,请使用wait.ForListeningPort()。如果容器内的服务需要时间进行初始化,您可以创建自定义就绪检查,例如检查容器日志或发送 HTTP 请求来验证状态。

使用轻量级镜像。如果测试不需要服务的全部功能,请选择轻量级容器镜像。这可以减少加载和启动时间。例如,带有-alpine后缀的镜像通常较小且启动速度更快。

容器日志输出
容器日志在诊断测试问题时非常有用。当测试失败并且您需要了解容器内部发生了什么时,这尤其有用。

logs, err := dbContainer.Logs(context.Background()) 
if err != nil { 
    t.Fatalf( "Error retrieving container logs: %s" , err) 

defer logs.Close() 

// 读取并输出日志
scanner := bufio.NewScanner(logs) 
for scanner.Scan() { 
    t.Log(scanner.Text()) // 输出日志的每一行
}

此代码允许您实时读取和输出容器日志,这有助于了解容器内部发生的情况。例如,如果数据库未正确启动,日志可能包含有用的错误消息或警告以供诊断。

结论
Testcontainers for Go 通过为每个测试创建独立且可重复的环境,大大简化了测试实际服务的过程。这有助于避免与本地基础设施相关的问题,并使测试更加可靠和稳定。无论您测试的是数据库、Web 服务还是消息队列,Testcontainers 都提供了灵活的工具来创建必要的环境,从而使集成测试更加简单。

使用容器进行测试是一种现代而强大的方法,可让您在与生产条件极为相似的条件下测试应用程序。借助 Testcontainers,您的测试不仅变得更加高效,而且还可以帮助您在问题进入生产之前发现并解决问题。