Golang Testcontainers模块实战指南

发表时间: 2024-08-06 10:18

介绍

Testcontainers for Go使开发人员能够轻松地针对容器化依赖项运行测试。在我们之前的文章中,您可以找到使用 Testcontainers 进行集成测试的介绍,并探索如何使用 Testcontainers(用 Java)编写功能测试。

这篇博文将深入探讨如何使用模块以及 Golang 测试容器的常见问题。

我们用它做什么?

服务经常使用外部依赖项,如数据存储或队列。可以模拟这些依赖项,但如果您想要运行集成测试,最好根据实际依赖项(或足够接近)进行验证。

使用依赖项的映像启动容器是验证应用程序是否按预期运行的便捷方法。使用 Testcontainers,启动容器是通过编程方式完成的,因此您可以将其定义为测试的一部分。运行测试的机器(开发人员、CI/CD)需要具有容器运行时接口(例如 Docker、Podman...)

基本实现

Testcontainers for Go 非常易于使用,快速启动示例如下:

ctx := context.TODO()req := testcontainers.ContainerRequest{    Image:        "redis:latest",    ExposedPorts: []string{"6379/tcp"},    WaitingFor:   wait.ForLog("Ready to accept connections"),}redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{    ContainerRequest: req,    Started:          true,})if err != nil {    panic(err)}defer func() {    if err := redisC.Terminate(ctx); err != nil {        panic(err)    }}()

如果我们深入研究上面的代码,我们会注意到:

  1. testcontainers.ContainerRequest使用容器镜像、暴露端口和等待策略参数初始化结构体
  2. testcontainers.GenericContainer启动容器并返回容器和错误结构
  3. redisC.Terminatedefer测试完成后终止容器

实现我们自己的内部库

从上一节的例子来看,存在一些小小的不便:

  1. wait.ForLog("Ready to accept connections")使用日志等待容器启动,这很容易中断
  2. ExposedPorts: []string{"6379/tcp"}需要了解 Redis 的暴露端口

运行 Redis 容器可能还需要一些额外的环境变量和其他参数,这需要更深入的知识。因此,我们决定创建一个内部库,该库将使用简化测试实施所需的默认参数初始化容器。为了保持灵活性,我们使用了功能选项模式,以便消费者仍然可以根据需要进行自定义。

Redis 的实现示例:

func defaultPreset() []container.Option {    return []container.Option{        container.WithPort("6379/tcp"),        container.WithGetURL(func(port nat.Port) string {            return "localhost:" + port.Port()        }),        container.WithImage("redis"),        container.WithWaitingStrategy(func(c *container.Container) wait.Strategy {            return wait.ForAll(                wait.NewHostPortStrategy(c.Port),                wait.ForLog("Ready to accept connections"))        }),    }}// New - create a new container able to run redisfunc New(options ...container.Option) (*container.Container, error) {    c := container.Container{}    options = append(defaultPreset(), options...)    for _, o := range options {        o(&c)    }    return &c, nil}// Start - start a Redis container and return a container.CreatedContainerfunc Start(ctx context.Context, options ...container.Option) (container.CreatedContainer, error) {    p, err := New(options...)    if err != nil {        return container.CreatedContainer{}, err    }    return p.Start(ctx)}

Redis 库的使用:

ctx := context.TODO()cc, err := redis.Start(ctx, container.WithVersion("latest"))if err != nil {    panic(err)}defer func() {    if err := cc.Stop(ctx, nil); err != nil {        panic(err)    }}()

有了这个内部库,开发人员可以轻松地为 Redis 添加测试,而无需弄清楚等待策略、暴露端口等。如果出现不兼容的情况,可以更新内部库以集中修复问题。

常见问题 - 垃圾收集器(Ryuk / Reaper)

Testcontainers 还额外确保了测试完成后容器会被移除,它使用垃圾收集器defer,这是一个作为“sidecar”启动的附加容器。即使测试崩溃(这将阻止运行),此容器也会负责停止正在测试的容器。

当使用Docker时,它可以正常工作,但使用其他容器运行时接口(如Podman)时经常会遇到这种错误:Error response from daemon: container create: statfs /var/run/docker.sock: permission denied: creating reaper failed: failed to create container

“修复此问题”的一种方法是使用环境变量将其停用
TESTCONTAINERS_RYUK_DISABLED=true

另一种方法是设置 Podman 机器 rootful 并添加:

export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true; # needed to run Reaper (alternative disable it TESTCONTAINERS_RYUK_DISABLED=true)export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock; # needed to apply the bind with statfs

在我们的内部库中,我们采取默认禁用它的方法,因为开发人员在本地运行它时遇到了问题。

转向模块

一旦我们的内部库足够稳定,我们就决定是时候通过为 Testcontainers 做贡献来回馈社区了。但令人惊讶的是…… Testcontainers 刚刚引入了模块。模块的功能与我们的内部库完全一样,因此我们将所有服务迁移到模块并停止使用内部库。从迁移中,我们了解到,既然已经引入了模块,就可以使用开箱即用的标准库,从而降低了我们服务的维护成本。主要的挑战是使用 Makefile 微调开发人员环境变量以在开发人员机器上运行(使垃圾收集器工作)。

改编自testcontainers 文档的示例:

ctx := context.TODO()redisContainer, err := redis.RunContainer(ctx,    testcontainers.WithImage("docker.io/redis:latest"),)if err != nil {    panic(err)}defer func() {    if err := redisContainer.Terminate(ctx); err != nil {        panic(err)    }}()

结论

Testcontainers for Golang 是一个很棒的支持测试的库,现在引入了模块,它变得更好了。垃圾收集器存在一些小障碍,但可以按照本文所述轻松修复。

我希望通过这个博客,如果您还没有采用 Testcontainers,我们强烈推荐它来提高您的应用程序的可测试性。


作者:Fabien Pozzobon

出处
:https://engineering.zalando.com/posts/2023/12/using-modules-for-testcontainers-with-golang.html