testcontainers-go不支持绑定卷时,如何用Go测试Docker容器文件输出?
问题翻译
我用Go开发了一款CLI应用,能从源码文件和行内注释生成AsciiDoc文件,该应用以Docker镜像形式分发。测试阶段计划结合Godog做BDD测试,需要启动带测试数据的Docker容器运行这个应用,再对生成的AsciiDoc文件做断言验证。原本打算用testcontainers-go实现,但看其官方文档说该工具不支持绑定卷。因为容器执行完命令就会终止,没法查看或提取容器内的文件,所以希望用类似docker run ... --volume some/path:some/path ...的绑定卷方式,让容器正常终止后,还能在宿主机文件系统上对结果做断言。请问能不能通过testcontainers-go实现这个需求?如果不能,还有哪些用Go测试Docker容器文件输出的替代方案?
解答
一、testcontainers-go 完全支持绑定卷
你看到的文档信息可能是旧版本或表述偏差,testcontainers-go 早就支持通过绑定卷实现宿主机与容器的目录映射。即使容器执行完成后终止,宿主机挂载目录依然会保留容器生成的文件,完全满足你的断言需求。
示例代码如下:
import ( "context" "os" "path/filepath" "strings" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) func TestCLIAppAsciiDocOutput(t *testing.T) { ctx := context.Background() // 创建临时目录用于存放生成的AsciiDoc文件 outputDir, err := os.MkdirTemp("", "cli-test-output") if err != nil { t.Fatal(err) } defer os.RemoveAll(outputDir) // 获取宿主机测试数据目录的绝对路径 testDataDir, err := filepath.Abs("./test-data") if err != nil { t.Fatal(err) } // 定义容器请求配置 containerReq := testcontainers.ContainerRequest{ Image: "your-cli-app-image:latest", Cmd: []string{"generate", "/input", "/output"}, // 你的CLI应用执行命令 Mounts: testcontainers.Mounts( // 绑定测试数据目录到容器内的/input路径 testcontainers.BindMount(testDataDir, "/input"), // 绑定宿主机临时目录到容器内的/output路径 testcontainers.BindMount(outputDir, "/output"), ), WaitingFor: wait.ForExit(), // 等待容器执行完成后退出 } // 创建并启动容器 container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: containerReq, Started: true, }) if err != nil { t.Fatal(err) } defer container.Terminate(ctx) // 读取宿主机临时目录中的生成文件,执行断言 generatedFile := filepath.Join(outputDir, "result.adoc") content, err := os.ReadFile(generatedFile) if err != nil { t.Fatalf("Failed to read generated file: %v", err) } // 示例断言:检查生成内容是否包含预期文本 if !strings.Contains(string(content), "Expected AsciiDoc Section") { t.Error("Generated AsciiDoc does not match expected content") } }
二、替代方案
如果因特殊场景不想使用testcontainers-go,还有两种常用方案:
1. 直接调用Docker CLI命令
用Go的os/exec包执行原生docker run命令,带上--volume参数实现目录绑定,容器终止后直接读取宿主机挂载目录的文件做断言。这种方式完全模拟手动操作,适合简单测试场景。
示例代码:
import ( "os" "os/exec" "path/filepath" "strings" "testing" ) func TestCLIWithDockerCLI(t *testing.T) { outputDir, err := os.MkdirTemp("", "docker-cli-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(outputDir) testDataDir, _ := filepath.Abs("./test-data") // 执行docker run命令,绑定卷并运行CLI应用 cmd := exec.Command("docker", "run", "--rm", "-v", testDataDir+":/input", "-v", outputDir+":/output", "your-cli-app-image:latest", "generate", "/input", "/output") if err := cmd.Run(); err != nil { t.Fatalf("Docker run failed: %v", err) } // 读取生成文件并断言 generatedFile := filepath.Join(outputDir, "result.adoc") content, _ := os.ReadFile(generatedFile) if !strings.Contains(string(content), "Expected Content") { t.Error("Generated AsciiDoc is invalid") } }
2. 使用Docker Go SDK
通过官方Docker SDK for Go直接操作容器,配置绑定卷、启动容器并等待退出,之后读取宿主机挂载的文件。这种方式比调用CLI更灵活,能更好地集成到Go测试流程中。
示例代码片段:
import ( "context" "os" "path/filepath" "strings" "testing" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" ) func TestCLIWithDockerSDK(t *testing.T) { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { t.Fatal(err) } defer cli.Close() outputDir, err := os.MkdirTemp("", "docker-sdk-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(outputDir) testDataDir, _ := filepath.Abs("./test-data") // 配置容器主机绑定卷 hostConfig := &container.HostConfig{ Binds: []string{ testDataDir + ":/input", outputDir + ":/output", }, } // 创建容器 resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: "your-cli-app-image:latest", Cmd: []string{"generate", "/input", "/output"}, }, hostConfig, nil, nil, "") if err != nil { t.Fatal(err) } defer cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true}) // 启动容器 if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { t.Fatal(err) } // 等待容器执行完成退出 _, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) if err := <-errCh; err != nil { t.Fatal(err) } // 读取生成文件并断言 generatedFile := filepath.Join(outputDir, "result.adoc") content, _ := os.ReadFile(generatedFile) if !strings.Contains(string(content), "Expected AsciiDoc Content") { t.Error("Generated file does not meet requirements") } }
内容的提问来源于stack exchange,提问作者Sebastian Sommerfeld

