DevGang
Авторизоваться

Тестирование Golang с использованием сервисов докеров через dockertest

За время своего обучения я наткнулся на несколько замечательных библиотек и утилит, одна из моих любимых для интеграционного тестирования — 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 позволяет нам создать полностью чистый изолированный экземпляр службы, поэтому мы знаем, что каждый раз начинаем с нуля. Это следует из идеологии cattle not pets от Devops.

Здесь мы просто обсуждаем postgres, но для некоторых других служб докеров может потребоваться МНОГО ручных действий, чтобы привести их в хорошее состояние для тестовых запусков.

Пример

Я создал здесь пример проекта, чтобы показать все в действии, но буду прорабатывать каждый сделанный шаг.

Проект, который я создал, очень прост, и основная работа заключается в следующем коде:

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 Compose), результат увеличивает счетчик при каждом запуске (как и должно быть).

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)
}

Итак, сначала мы создаем новый пул докеров, убеждаемся, что пул отвечает на пинг, а затем запускаем экземпляр postgres. Когда вы запускаете новый экземпляр, он захватывает случайный порт, поэтому вам нужен способ передать его в службу, поэтому у нас есть переменная окружения POSTGRES_PORT. В самом конце настройки модуля мы устанавливаем эту среду, чтобы тест использовал ее.

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

Существует раздел ожидания готовности postgres, вы можете использовать его по-разному, но в основном вы используете какое-то условие для проверки готовности экземпляра. Это может быть что угодно: от проверки адресности порта до вызова проверки работоспособности http.

    // 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 секунд.

Наконец-то у нас есть настоящий тест

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)
    }
}

Источник:

#Golang #Docker
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу