У нас был сервис на golang, отдельный топик kafka, clickhouse, gitlab-ci и падающий пайплайн, протухший ssh-ключ и вот это вот все, а еще сезон отпусков, жуткие ливни в городе, сломавшийся ноутбук, алерты по ночам, и горящий прод. Не то, чтобы это все было нужно для этой статьи, но раз показываешь типичные будни тестировщика, то иди в своем намерении до конца. Единственное, что меня беспокоило — это p0. В мире нет ничего более отчаянного, мрачного и подавленного, чем тестировщик, который пропустил это на прод. Но я знала, что довольно скоро я в это окунусь.
Зачем все это?
Cуществует распространенная связка сервисов — сам сервис, который что-то делает, — и база данных, в которую эти результаты записываются. иногда это происходит напрямую, то есть “сервис — база”. В моем случае запись происходит через посредника, то есть “сервис — очередь — база”.
Итого, есть несколько элементов, и граница этих элементов — выход одного и вход другого — это то самое место, где появляются проблемы. Они просто не могут там не появиться.
Яркий пример: в сервисе поле price обрабатывается как float32, в базе оно настроено как decimal(18, 5), подаем с выхода сервиса на базу максимальное значение float32 в качестве тесткейса — ой, база не отвечает. Или уже более грустный пример — база не крашится, но в логах ошибки записи данных в базу нет. просто в базу данные перестают наливаться. Или запись проходит, но с потерей данных или с искажением: поле выходит из сервиса как float64, а записывается как float32.
Или в процессе жизненного цикла сервиса решили, что надо поменять тип того или иного поля. Поле уже давно реализовано на проде, но вот необходимо его отредактировать. И конечно же поменяли это только в одном месте. Хоба, что-то опять пошло не так.
Задача
Я не хочу следить за всеми этими изменениями. Я хочу, чтобы оно не падало. Я хочу, чтобы запись проходила корректно.
Выход: интеграционные тесты!
Реализация и трудности
Где ломать?
Есть dev-окружение: жутко нестабильное и обычно используется разработчиками как песочница. Там творится хаос и анархия, характерные для жесткого бекенда.
Есть test-окружение или qa-стенд: настроено уже получше, за ним даже следят devops, но пока их не пнешь, ничего не произойдет. и еще это окружение часто обновляется. а еще чаще там что-то сломано.
И есть прод — святая святых: на нем лучше ничего подобного не гонять. интеграционные тесты предполагают возможность наличия бага, который они должны найти до того, как он попадет на прод.
Так что же делать с окружением, когда оно или нестабильное, или боевое? Правильно, создавать свое!
Что делать с базой?
Базу можно запускать несколькими способами.
Как мы уже обговорили выше, к реальной базе того или иного окружения мы подключаться не будем.
Во-первых, можно поднять покостылить clickhouse-server с нужными настройками, раскатать на нем необходимые sql и общаться с ним посредством clickhouse-client. На первой же успешной попытке положить подобную базу, загрустил и ci. тесты зафейлились, сервер не потух и продолжил работать. Скажем так, до меня до сих пор остается загадкой, почему оно вообще запустилось. (оно само, я ни при чем). Не советую этот вариант.
Удобный вариант из коробки — использование docker образа.
Скачиваем нужную версию к себе на машину. Clickhouse для старта нужен config.xml с настройками. Подробнее тут
Для переиспользуемого образа клика надо создать правильный dockerfile. Указываем в нем, что хотим скопировать config.xl в папку, докидываем другие требуемые конфиги. Обязательно копируем скрипты для разворачивания своей базы.
Так как к образу мы будем обращаться извне, то надо открыть те порты, по которым будем общаться с кликхаусом. Клик работает на 8123 по http и на 9000 по tcp.
Получаем следующий dockerfile:
From yandex/clickhouse-server
Expose 8123
Expose 9000
Add config.xml /etc/clickhouse-server/config.xml
Add my_init_script.sql /docker-entrypoint-initdb.d/
Как закинуть образ в ci?
Чтобы с docker-образом как-то работать в ci, его надо там как-то вызвать.
Можно закоммитить и запушить образ в свой репозиторий и в рамках запуска тестов выполнять docker run с нужными параметрами. Только вот docker-образ клика весит под 350мб. неприлично такие файлы держать в git.
Кроме того, если один и тот же docker-образ нужен на разных проектах (например, разные сервисы пишут в одну и ту же базу), то тем более так делать не стоит. Можно использовать хранилище образов docker registry
Считаем, что в нашем проекте он уже есть и используется. Поэтому логинимся, собираем docker-образ и пушим его туда.
docker build -t my_clickhouse_image .
docker login my_registry_path.domain.com
docker push my_clickhouse_image
Вжух и наш образ улетел в registry. Обязательно указываем тег при сборке!
База готова.
Подробнее про registry тут
Что делать с ci ?
Как в рамках одного шага запустить и свой сервис, и базу?
Все зависит от того, как у нас запускается и используется сервис. Если с сервисом работать как с docker-образом, да и вообще весь .gitlab-ci.yml работает только с ними, то все просто.
Существует приблуда dind — docker-in-docker. Она указывается как основной сервис, с которым работает ci, и позволяет и докером полноценно пользоваться, и вообще не напрягаться.
Выкачиваем самый свежий образ, добавляем в stages шаг требуемого тестирования и описываем свою последовательность действий.
image: docker:stable
services:
- docker:dind
stages:
- build
…
- test-click
...
- test
- release
…
test-click:
variables:
VERY_IMPORTANT_VARIABLE: “its value”
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull My_Service_Image
- docker pull My_ClickHouse_Image
- docker run -FLAGS My_ClickHouse_Image
- docker run My_Service_Image /path/to/tests
В официальном докере докера указывается, что не рекомендуется использовать dind, но если очень надо…
В моем проекте сервис надо тестировать через запуск бинарника. Тут и начинается магия
Для этого нужно использовать базу как сервис. Официальная документация gitlab-ci приводит использование контейнера с базой в качестве примера самого распространенного варианта использования docker-контейнера в ci. Даже приведены примеры настроек mysql, postress и redis. Но мы же не ищем легких путей, нам нужен clickhouse.
Подключаем базу! Обязательно указываем alias. если его не указать, то базе будет приписываться рандомное имя и рандомный ip. То есть, будет непонятно, как именно к ней обращаться. С alias такой проблемы не будет — в коде тестов обращение будет выглядеть как, например, по хттп http://my_alias_name:8123
.
Для тестов все так же требуется образ базы, который мы старательно запушили в registry. для скачивания образа необходимо выполнить docker login и docker pull, только ci не знает, что такое docker — надо его установить.
Итоговый код для шага в gitlab-ci.yml:
Integration tests:
Services:
- name: my_clickhouse:latest
alias: clicktest
Stage: tests
Variables:
Variables_for_my_service: “value”
Before_script:
- curl -ssl https://get.docker.com/ | sh
- docker login -u gitlab-ci-token -p $ci_build_token my_registry_path.domain.com
Script:
- ./bin/my_service &
- go test -v ./tests -tags=integration
Dependencies:
- build
Профит
- У меня есть работающая связка сервис-базка.
- В рамках автотеста легко обратиться к базе — просто по alias.
- Обнуляю записи и настройки базы в рамках setup теста, вызываю работу сервиса, он пишет в базу, обращаюсь к базе, смотрю, что база не отвалилась, смотрю, что пришло, валидирую. накидываю побольше тестов.
- Можно ручками не тестировать!
Результаты
Казалось бы, пара строчек настройки в gitlab-ci. Собрать docker образ — просто. Запустить базку локально — просто. у меня за сутки появилась интеграция с первыми тестами, которые нашли проблемы. Но попытки запустить в это в ci обратились в неделю боли и безысходности. А теперь и в недели боли и безысходности разработчиков, которым придется чинить все, что они там напрограммировали.
Что мы сумели сделать?
- Мы настроили контейнер с clickhouse.
- Запушили контейнер в локальное хранилище.
- Научились подтягивать этот образ в шаг ci.
- Запустили его в раннере.
Легко отправили данные в базу и обратились к ней из теста.
Автоматизация — это довольно простой способ избавить себя от рутины ручного протыкивания интеграции.
На что важно обратить внимание: проследите, что входные типы базы соответствуют выходным типам предыдущего звена. (и документации, если таковая имеется).
Автор: TheGingerHAL