Categories: Go

golang-dockertest/

Допустим, мы разрабатываем микросервис на языке Go . Мы успешно написали модульные тесты . Но также требуется написать и другие тесты, которые проверяли бы, что посылка определенной серии запросов к сервису приводит к получению ожидаемых ответов. Обычно такие тесты называют интеграционными . Существует более одного решения задачи. Можно поднимать стенды со всеми зависимостями микросервиса (или чем-то, что ими притворяется), что практически сводит задачу к системному тестированию. Или наоборот, можно замокать все зависимости, и свести задачу к модульному тестированию. Но в рамках этой заметки мне хотелось бы рассказать о решении, основанном на использовании Docker и библиотеки dockertest .

Для определенности будем писать тесты к приложению, описанному в посте Работа с PostgreSQL в языке Go при помощи pgx . Напомню, что приложение представляет собой телефонную книгу с REST-интерфейсом . Для работы приложению нужен PostgreSQL , других зависимостей у него нет.

По большому счету, dockertest представляет собой библиотеку для работы с Docker из языка Go:

func StartPostgreSQL () ( confPath string , cleaner func ()) {
pool , err := dockertest . NewPool ( «» )
if err != nil {
log . Panicf ( «dockertest.NewPool failed: %v» , err )
}

resource , err := pool . Run (
«postgres» , «11» ,
[] string {
«POSTGRES_DB=restservice» ,
«POSTGRES_PASSWORD=s3cr3t» ,
},
)
if err != nil {
log . Panicf ( «pool.Run failed: %v» , err )
}
// …

Здесь мы запускаем контейнер с PostgreSQL. Перед запуском тестов нужно дождаться, когда постгрес реально поднимется:

// …

// PostgreSQL needs some time to start.
// Port forwarding always works, thus net.Dial can’t be used here.
connString := «postgres://postgres:s3cr3t@» +
resource . GetHostPort ( «5432/tcp» ) +
«/restservice?sslmode=disable»
attempt := 0
ok := false
for attempt < 20 {
attempt ++
conn , err := pgx . Connect ( context . Background (), connString )
if err != nil {
log . Infof ( «pgx.Connect failed: %v, waiting… (attempt %d)» ,
err , attempt )
time . Sleep ( 1 * time . Second )
continue
}

_ = conn . Close ( context . Background ())
ok = true
break
}

if ! ok {
_ = pool . Purge ( resource )
log . Panicf ( «Couldn’t connect to PostgreSQL» )
}

// …

Теперь мы знаем, что постгрес запущен, а также знаем, на каком порту его искать. Сгенерируем соответствующий файл конфигурации для нашего сервиса:

// …

tmpl , err := template . New ( «config» ) . Parse ( `
loglevel: debug
listen: 0.0.0.0:8080
db:
url: {{.ConnString}}
`
)
if err != nil {
_ = pool . Purge ( resource )
log . Panicf ( «template.Parse failed: %v» , err )
}

configArgs := struct {
ConnString string
} {
ConnString : connString ,
}
var configBuff bytes. Buffer
err = tmpl . Execute ( &configBuff , configArgs )
if err != nil {
_ = pool . Purge ( resource )
log . Panicf ( «tmpl.Execute failed: %v» , err )
}

confFile , err := ioutil . TempFile ( «» , «config.*.yaml» )
if err != nil {
_ = pool . Purge ( resource )
log . Panicf ( «ioutil.TempFile failed: %v» , err )
}

log . Infof ( «confFile.Name = %s» , confFile . Name ())

_ , err = confFile . WriteString ( configBuff . String ())
if err != nil {
_ = pool . Purge ( resource )
log . Panicf ( «confFile.WriteString failed: %v» , err )
}

err = confFile . Close ()
if err != nil {
_ = pool . Purge ( resource )
log . Panicf ( «confFile.Close failed: %v» , err )
}

// …

Возвращаем путь к файлу конфигурации, а также функцию, освобождающую ресурсы:

// …

cleanerFunc := func () {
// purge the container
err := pool . Purge ( resource )
if err != nil {
log . Panicf ( «pool.Purge failed: %v» , err )
}

err = os . Remove ( confFile . Name ())
if err != nil {
log . Panicf ( «os.Remove failed: %v» , err )
}
}

return confFile . Name (), cleanerFunc
}

Соответственно, перед запуском тестов, в процедуре TestMain нам необходимо запустить сервис со сгенеренным файлом конфигурации:

func TestMain ( m * testing . M ) {
log . Infoln ( «About to start PostgreSQL…» )
confPath , stopPostgreSQL := StartPostgreSQL ()
log . Infoln ( «PostgreSQL started!» )

// We should change the directory, otherwise the service will
// not find `migrations` directory
err := os . Chdir ( «../..» )
if err != nil {
stopPostgreSQL ()
log . Panicf ( «os.Chdir failed: %v» , err )
}

cmd := exec . Command ( «./bin/rest-service-example» , «-c» , confPath )
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
err = cmd . Start ()
if err != nil {
stopPostgreSQL ()
log . Panicf ( «cmd.Start failed: %v» , err )
}
log . Infof ( «cmd.Process.Pid = %d» , cmd . Process . Pid )

// …

Необходимо дождаться готовности API. Дело в том, что первым делом сервис выполняем миграцию схемы базы данных. Если сейчас мы перейдем к тестам, они все завершатся с ошибками, после чего мы остановим Docker-контейнер, что приведет к аварийному завершению сервиса. Такая вот интересная гонка. Код ожидания готовности сервиса:

// …

attempt := 0
ok := false
client := httpClient {}
for attempt < 20 {
attempt ++
_ , _ , err := client . sendJsonReq (
«GET» ,
«http://localhost:8080/api/v1/records/0» ,
[] byte {})
if err != nil {
log . Infof (
«client.sendJsonReq failed: %v, waiting… (attempt %d)» ,
err , attempt )
time . Sleep ( 1 * time . Second )
continue
}

ok = true
break
}

if ! ok {
stopPostgreSQL ()
_ = cmd . Process . Kill ()
log . Panicf ( «REST API is unavailable» )
}

// …

Здесь httpClient — это небольшая обертка над стандартным http.Client . Он имеет незамысловатый интерфейс, упрощающий посылку REST-запросов и получение ответов.

Наконец, запускаем все тесты, а затем подчищаем за собой:

// …

log . Infoln ( «REST API ready! Executing m.Run()» )
// Run all tests
code := m . Run ()

log . Infoln ( «Cleaning up…» )
_ = cmd . Process . Signal ( syscall . SIGTERM )
stopPostgreSQL ()
os . Exit ( code )
}

Пример теста, проверяющего создание новых записей:

type PhonebookRecord struct {
Id int64 `json:»id»`
Name string `json:»name»`
Phone string `json:»phone»`
}
client := httpClient {}

record := PhonebookRecord {
Name : «Alice» ,
Phone : «123» ,
}
httpBody , err := json . Marshal ( record )
require . NoError ( t , err )
resp , respBody , err := client . sendJsonReq (
«POST» ,
«http://localhost:8080/api/v1/records» ,
httpBody )
require . NoError ( t , err )
require . Equal ( t , 200 , resp . StatusCode )
respBodyMap := make ( map [ string ] string , 1 )
err = json . Unmarshal ( respBody , &respBodyMap )
require . NoError ( t , err )
recId , err := strconv . ParseInt ( respBodyMap [ «id» ], 10 , 31 )
require . NoError ( t , err )
require . NotEqual ( t , 0 , recId )

Прочие тесты написаны аналогичным образом. Ознакомиться с полной версией кода можно в репозитории на GitHub .

Итак, чем же хорош dockertest? Тем, что любой разработчик может запустить интеграционные тесты локально, просто сказав go test ./... и нажав Enter. Нужен только Docker, а все остальное тест делает сам — поднимает чистый PostgreSQL, запускает на нем сервис, тестирует его, останавливает, подчищает за собой. Не нужно никаких стендов, не нужно писать никаких моков. Вы тестируете приложение в таком же состоянии, в каком оно будет работать на проде. Только все зависимости запускаются в контейнерах, а затем удаляются. Очень удобно!

Заинтересованным читателям предлагается попробовать dockertest самим, написав тесты на API GET /api/v1/records . Напомню, что этот API предлагалось реализовать в конце заметки Работа с PostgreSQL в языке Go при помощи pgx .

Дополнение: В продолжение темы см заметку Непрерывная интеграция с GitHub Actions .

admin

Share
Published by
admin
Tags: Go

Recent Posts

Консоль удаленного рабочего стола(rdp console)

Клиент удаленного рабочего стола (rdp) предоставляет нам возможность войти на сервер терминалов через консоль. Что…

1 месяц ago

Настройка сети в VMware Workstation

В VMware Workstation есть несколько способов настройки сети гостевой машины: 1) Bridged networking 2) Network…

1 месяц ago

Логи брандмауэра Windows

Встроенный брандмауэр Windows может не только остановить нежелательный трафик на вашем пороге, но и может…

1 месяц ago

Правильный способ отключения IPv6

Вопреки распространенному мнению, отключить IPv6 в Windows Vista и Server 2008 это не просто снять…

1 месяц ago

Ключи реестра Windows, отвечающие за параметры экранной заставки

Параметры экранной заставки для текущего пользователя можно править из системного реестра, для чего: Запустите редактор…

1 месяц ago

Как управлять журналами событий из командной строки

В этой статье расскажу про возможность просмотра журналов событий из командной строки. Эти возможности можно…

1 месяц ago