与测试方法分开在 docker 应用程序中运行 docker 映像

Posted

技术标签:

【中文标题】与测试方法分开在 docker 应用程序中运行 docker 映像【英文标题】:Run docker images in a docker application separately from test method 【发布时间】:2022-01-23 09:19:28 【问题描述】:

我有一个 docker 应用程序,它有两个容器。一个是 mysql,另一个是我创建的使用 Dockerfile 的自定义映像的一些逻辑代码。对于端到端测试,我希望在数据库中存储一些值,然后运行逻辑代码映像(Golang 中的逻辑)。这是我目前拥有的 docker-compose 文件:

version: '3'
networks:
  docker-network:
    driver: bridge
services:
    database:
        image: mysql
        env_file:
          - ./src/logic/environment-variables.env
        ports:
          - 3306:3306
        healthcheck:
          test: "mysql -uroot -p$$MYSQL_ROOT_PASSWORD $$MYSQL_DATABASE -e 'select 1'"
          timeout: 20s
          retries: 10
        network:
          docker-network
    logic:
        container_name: main-logic
        build: ./src/logic/.
        depends_on:
          database:
            condition: service_healthy
        network:
          docker-network

我不能作为一个整体运行这个应用程序,因为它会在数据库运行后立即运行主程序。相反,我想启动数据库,在其中存储一些值,然后运行逻辑映像。如何在测试方法中做到这一点?

考虑的方法: 与测试方法分开启动mysql映像,然后在其中存储值。然后启动逻辑映像并检查数据库以获取结果。有没有更好的方法或框架可以用于此?

【问题讨论】:

你想在你的 golang 应用程序启动之前正确初始化你的数据库吗? 是的。基本上需要数据库中有值,以便我可以测试逻辑图像中的代码 您可以为您的 MySQL 容器创建一个自定义 entrypoint.sh 脚本来运行迁移并构建/填充您的数据库...或者,如果您不使用任何 mysql 特定功能,请抽象驱动程序-type(IE:所以它可以是可变的)并使用磁盘上的 sqlite 数据库(IE:将其放在 testdata/ 目录中)并使用它来测试您的 DB 代码......或者只是模拟 DB 驱动程序 这能回答你的问题吗? Does docker-compose support init container? @TheFool 这在某种程度上确实如此,但即使在实际情况下,问题也需要该功能,而不仅仅是用于测试。但是对于我的用例,我只需要它来进行测试,而不是在其他情况下运行代码时。如果我使用它,那么 docker-compose 在所有情况下对我来说都包含不必要的开销 【参考方案1】:

您需要的是数据库迁移。这应该如下工作:

在启动服务之前启动数据库实例。 将服务连接到数据库。 在 DB 上运行迁移。 继续执行服务。

考虑一下:https://github.com/golang-migrate/migrate

【讨论】:

【参考方案2】:

您可以完全按照您在问题中所说的做:启动数据库,手动加载种子数据,然后启动应用程序的其余部分。由于您的数据库已发布ports:,您可以直接从主机连接到它,而无需做任何特殊操作。

docker-compose up -d database
mysql -h 127.0.0.1 < seed_data.sql
docker-compose up -d

@advayrajhansa 的answer 建议使用数据库迁移系统。如果这已内置到您的图像中,您可以将docker-compose run logic migrate ... 作为中间步骤。这会在您在 docker-compose.yml 文件中定义的容器上运行备用命令。

【讨论】:

我会试试这个。这是否意味着整个测试方法可以作为脚本运行并且不需要任何 go test funcs? 如果它是一种端到端集成测试,它向您的应用程序发出 HTTP 请求并评估输出,它不需要在容器内运行或使用与应用程序。但这似乎与“我如何为该测试设置初始数据”有点不同。【参考方案3】:

对于您的方法:

    启动 MySQL 映像。 将数据上传到数据库。 启动逻辑映像。 检查数据库以获取结果。

你可以:

使用 Makefile

里面有一个 sh 脚本,会一一执行所有步骤。

生成文件:

start_test:
    docker-compose run -d database
    # put here your data uploading script
    docker-compose run -d logic
    #  put here your data database checking script

然后执行

$make start_test # execute all steps

使用Testcontainers-Go

Testcontainers GitHub

Testcontainers-Go 是一个 Go 包,可以轻松创建和清理基于容器的依赖项以进行自动化集成/冒烟测试。

它允许您执行 go 测试方法中的所有步骤。 对于你的情况,你会有这样的事情:

只是一个草稿代码来赶上这个想法:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "github.com/pkg/errors"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
    "log"
    "testing"
)

var db *sql.DB

func TestIntegration(t *testing.T) 
    if testing.Short() 
        t.Skip("skipping integration test")
    
    
    err := setupMySql()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    
    
    err = setupData()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    
    err = setupLogic()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    
    err = checkResult()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    


func setupMySql() error 
    ctx := context.Background()
    req := testcontainers.ContainerRequest
        Image:        "mysql:latest",
        ExposedPorts: []string"3306/tcp", "33060/tcp",
        Env: map[string]string
            "MYSQL_ROOT_PASSWORD": "secret",
        ,
        WaitingFor: wait.ForLog("port: 3306  MySQL Community Server - GPL"),
    
    mysqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest
        ContainerRequest: req,
        Started:          true,
    )
    defer func() 
        err := mysqlC.Terminate(ctx)
        if err != nil 
            log.Fatal(err)
        
    ()
    
        if err != nil 
            return errors.Wrap(err, "Failed to run test container")
        
    
    host, err := mysqlC.Host(ctx)
    p, err := mysqlC.MappedPort(ctx, "3306/tcp")
    port := p.Int()
    connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?tls=skip-verify",
        "root", "secret", host, port, "database")
    
    db, err = sql.Open("mysql", connectionString)
    defer func(db *sql.DB) 
        err := db.Close()
        if err != nil 
            log.Fatal(err)
        
    (db)
    
    if err != nil 
        return errors.Wrap(err, "Failed to connect to db")
    
    
    return nil


func setupData() error 
    // db.Query(), your code with uploading data
    return nil


func setupLogic() error 
    // run your logic container
    return nil


func checkResult() error 
    // db.Query(), your code with checking result
    return nil

使用Dockertest

Dockertest 可帮助您以最少的工作量为您的 Go 测试启动临时 docker 映像。

与 Testcontainers-Go 相同,

只是一个草稿代码来赶上这个想法:

package main

import (
    "database/sql"
    "fmt"
    "github.com/ory/dockertest/v3"
    "github.com/pkg/errors"
    "testing"
)

var db *sql.DB

func TestIntegration(t *testing.T) 
    if testing.Short() 
        t.Skip("skipping integration test")
    
    
    err := setupMySql()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    
    
    err = setupData()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    
    err = setupLogic()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    
    err = checkResult()
    if err != nil 
        t.Errorf("Test failed with error: %s", err)
    


func setupMySql() error 
    // uses a sensible default on windows (tcp/http) and linux/osx (socket)
    pool, err := dockertest.NewPool("")
    if err != nil 
        return errors.Wrap(err, "Could not connect to docker")
    
    
    // pulls an image, creates a container based on it and runs it
    resource, err := pool.Run("mysql", "5.7", []string"MYSQL_ROOT_PASSWORD=secret")
    if err != nil 
        return errors.Wrap(err, "Could not start resource")
    
    
    // exponential backoff-retry, because the application in the container might not be ready to accept connections yet
    if err := pool.Retry(func() error 
        var err error
        db, err = sql.Open("mysql", fmt.Sprintf("root:secret@(localhost:%s)/mysql", resource.GetPort("3306/tcp")))
        if err != nil 
            return err
        
        return db.Ping()
    ); err != nil 
        return errors.Wrap(err, "Could not connect to database")
    
    
    if err := pool.Purge(resource); err != nil 
        return errors.Wrap(err, "Could not purge resource")
    
    
    return nil


func setupData() error 
    // db.Query(), your code with uploading data
    return nil


func setupLogic() error 
    // run your logic container
    return nil


func checkResult() error 
    // db.Query(), your code with checking result
    return nil

【讨论】:

以上是关于与测试方法分开在 docker 应用程序中运行 docker 映像的主要内容,如果未能解决你的问题,请参考以下文章

Docker--概述

Docker

Docker概览

CentOS 8.4安装Docker

一些测试运行后测试容器失去连接

进击的Docker2023年Docker快速入门教程,包含.Net项目的部署