Golang测试使用Dockertest使用Docker服务
#测试 #go #docker

在我的道路学习过程中,我遇到了一些令人惊叹的图书馆和Utilites,我最喜欢的集成测试之一是dockertest

每当我使用Postgres,Mongo,MySQL或其他不属于代码库一部分的服务的服务时,我通常会创建一个docker-compose文件,以便隔离我的开发环境。然后,当我在一个特定项目中工作时,我要做的就是docker-compose up -d开始,而docker-compose down当我完成一天后。

例如,如果我只需要一个Postgres数据库,我通常会有类似
的东西

services:
  postgres:
    image: postgres:15
    ports:
      - 5432:5432
    volumes:
      - pg-db:/var/lib/postgresql/data
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust
      POSTGRES_DB: example

volumes:
  pg-data:

这非常适合开发,但是在运行集成测试时,如果我想要一个干净的数据库,则意味着我需要一些其他步骤。我可以:

  • 手动删除并创建数据库
  • 删除并创建数据库作为集成测试的一部分
  • 许多其他选项
DockerTest

而不是创建一些自动化以删除和创建数据库或表,而是让我们创建一个完整的清洁隔离实例,因此我们知道每次都不毫无疑问。这是从Devops中的cattle not pets理想学进行的。

我们只是在这里讨论Postgres,但是其他一些Docker Services可能需要很多手动步骤才能进入测试运行状况良好。

例子

我创建了一个示例项目here来显示所有行动,但是我将努力完成我采取的每个步骤。

我创建的项目非常简单,主要工作是在以下代码中:

package database

import (
    "fmt"
    "log"
    "os"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type (
    Person struct {
        gorm.Model
        Name string
        Age  uint
    }
)

var db *gorm.DB

func Connect() {
    log.Println("Setting up the database")

    pgUrl := fmt.Sprintf("postgresql://postgres@127.0.0.1:%s/example", os.Getenv("POSTGRES_PORT"))
    log.Printf("Connecting to %s\n", pgUrl)
    var err error

    db, err = gorm.Open(postgres.Open(pgUrl), &gorm.Config{})

    if err != nil {
        panic("failed to connect database")
    }

    // Migrate the schema
    db.AutoMigrate(&Person{})
}

func CreatePerson() {
    log.Println("Creating a new person in the database")
    person := Person{Name: "Danny", Age: 42}
    db.Create(&person)

    log.Println("Trying to write a new person to the database")
}

func CountPeople() int {
    var count int64
    db.Model(&Person{}).Count(&count)
    return int(count)
}

因此,我们有一种连接的方法,一种创建新人记录的方法以及一种计算记录的方法。这些都是从主入口点调用的

func main() {
    database.Connect()

    database.CreatePerson()

    count := database.CountPeople()

    log.Printf("Database has %d people", count)
}

如果我从命令行(一旦docker组成了),则结果会增加每次运行的计数(如它应该)

go-dockertest-example λ git main → go run main.go
2023/09/03 13:41:06 Setting up the database
2023/09/03 13:41:06 Connecting to postgresql://postgres@127.0.0.1:5432/example
2023/09/03 13:41:06 Creating a new person in the database
2023/09/03 13:41:06 Trying to write a new person to the database
2023/09/03 13:41:06 Database has 3 people

go-dockertest-example λ git main → go run main.go
2023/09/03 13:41:07 Setting up the database
2023/09/03 13:41:07 Connecting to postgresql://postgres@127.0.0.1:5432/example
2023/09/03 13:41:08 Creating a new person in the database
2023/09/03 13:41:08 Trying to write a new person to the database
2023/09/03 13:41:08 Database has 4 people

考试

现在让我们看看测试。首先是Dockertest的设置:

func TestMain(m *testing.M) {
    // Start a new docker pool
    pool, err := dockertest.NewPool("")
    if err != nil {
        log.Fatalf("Could not construct pool: %s", err)
    }

    // Uses pool to try to connect to Docker
    err = pool.Client.Ping()
    if err != nil {
        log.Fatalf("Could not connect to Docker: %s", err)
    }

    pg, err := pool.RunWithOptions(&dockertest.RunOptions{
        Repository: "postgres",
        Tag:        "15",
        Env: []string{
            "POSTGRES_DB=example",
            "POSTGRES_HOST_AUTH_METHOD=trust",
            "listen_addresses = '*'",
        },
    }, func(config *docker.HostConfig) {
        // set AutoRemove to true so that stopped container goes away by itself
        config.AutoRemove = true
        config.RestartPolicy = docker.RestartPolicy{
            Name: "no",
        }
    })

    if err != nil {
        log.Fatalf("Could not start resource: %s", err)
    }

    pg.Expire(10)

    // Set this so our app can use it
    postgresPort := pg.GetPort("5432/tcp")
    os.Setenv("POSTGRES_PORT", postgresPort)

    // Wait for the Postgres to be ready
    if err := pool.Retry(func() error {
        _, connErr := gorm.Open(postgres.Open(fmt.Sprintf("postgresql://postgres@localhost:%s/example", postgresPort)), &gorm.Config{})
        if connErr != nil {
            return connErr
        }

        return nil
    }); err != nil {
        panic("Could not connect to postgres: " + err.Error())
    }

    code := m.Run()

    os.Exit(code)
}

因此,首先我们创建一个新的Docker池,确保池对Ping响应,然后启动Postgres实例。当您启动新实例时,它将抓住一个随机端口,因此您需要一种将其传递给服务的方法,这就是为什么我们拥有POSTGRES_PORT env var的原因。在模块设置的末尾,我们设置了ENV,以便测试将使用它。

    // Set this so our app can use it
    postgresPort := pg.GetPort("5432/tcp")
    os.Setenv("POSTGRES_PORT", postgresPort)

有一个以等待Postgres准备就绪的部分,您可以以不同的方式使用它,但是基本上您正在使用某种条件来测试实例已准备就绪。这可能是从检查端口是可寻址的,到致电HTTP HealthCheck。

    // Wait for the Postgres to be ready
    if err := pool.Retry(func() error {
        _, connErr := gorm.Open(postgres.Open(fmt.Sprintf("postgresql://postgres@localhost:%s/example", postgresPort)), &gorm.Config{})
        if connErr != nil {
            return connErr
        }

        return nil
    }); err != nil {
        panic("Could not connect to postgres: " + err.Error())
    }

然后有:

  // Make sure we expire the instance after 10 seconds
    postgres.Expire(10)

这确保如果清理未成功,我们将在10秒后删除Docker实例。

最后,我们有实际的测试

func TestCreatePerson(t *testing.T) {
    // Connect to the database
    database.Connect()

    // Create a person in the database
    database.CreatePerson()

    // Check that the person was created
    count := database.CountPeople()

    if count != 1 {
        t.Errorf("Expected 1 person to be in the database, got %d", count)
    }
}

每次测试运行时,数据库中只有一个记录。