Создаём инфраструктуру для интеграционных тестов: делаем образы и подводим итоги
Привет! Это вторая и (пока что) последняя статья из цикла про создание инфраструктуры интеграционных тестов. Первая была здесь.
В прошлый раз мы выяснили:
зачем команда Fiji из 2ГИС решила переизобрести инфраструктуру интеграционных тестов;
какие вообще бывают окружения для автотестов, и в каких случаях их применять;
какое решение мы выбрали и почему;
как мы использовали ресурсы K8s и взаимодействовали с ними из кода.
Стек команды Fiji — C#, NUnit. Непрерывную интеграцию делаем на TeamCity, отчёты по автотестам смотрим в Allure. Базы данных для интеграционных тестов поднимаем в Docker-контейнерах внутри Kubernetes.
В этой части статьи — подготовка образов тестовой базы данных. Будет много Докера и практики. К чёрту предисловия, погнали!
Готовим образ тестовой базы данных в Docker
Сборка образа напоминает лабораторную работу в универе: берёшь чей-то готовый вариант и докручиваешь под свои нужды.
В мире Докера основной банк таких вариантов — докерхаб. Лезем туда и качаем базовый образ. Возможно, в будущем сделаем ему наследников. Если планируете всерьёз собирать образы и делиться ими, подумайте о развертывании docker registry внутри корпоративной сети.
На докерхабе есть официальные образы и неофициальные. Правило одно — не берите вслепую. Изучите докерфайл — что происходило с системой до вас. Проверьте настройки внутри образа. У меня был случай, когда в контейнере неверно отображались арабские символы — нужно было поиграть с настройками сервера.
Когда почувствуете себя в контейнере как дома, начнётся самая интересная задача. У вас на руках базовый образ и большая мечта про тестовые данные в Докере. Надо понять, как одно превратить в другое.
Сегодня обсудим, какие варианты для этого есть. Важно помнить, что универсального рецепта нет — выберите решение под себя или придумайте свой собственный способ.
Нехитрые рассуждения приводят нас к четырём основным стратегиям подготовки образа для тестовой базы данных. Разберём каждую из них.
1. Не делаем образы
При поднятии контейнера нужно будет сконфигурировать внутри образа переменные среды и прочие настройки. Вы будете создавать схему данных из кода, а потом ещё наполнять данными. И делать это каждый раз при запуске тестов.
Этот способ хорошо подойдёт, если:
ваши сервисы умеют накатывать инициирующие миграции;
у вас простая база и для тестов нужно мало данных.
2. Образ без данных
Берём базовый образ. Конфигурируем настройки, подкладываем необходимые плагины, накатываем схему данных. Создаем новый образ с готовой к работе базой данных, в которой пока нет тестовых данных. Будем создавать их из кода.
Этот способ подойдёт, если:
при запуске тестов вы не хотите готовить окружение — только создавать тестовые данные;
вам не нужно создавать специфические и сложно связанные тестовые данные.
3. Образ с реальными данными
А если просто взять нашу боевую базу данных и засунуть её внутрь образа? Когда он устареет, просто сделаем всё заново. Да, в нём будут лишние данные, но зато всё под рукой.
Можно исхитриться и засунуть в образ только часть реальных данных. Другой вопрос — как отделить эту самую часть, не потеряв в целостности данных? Например, для нас вырезать с карты отдельный город — очень сложная задача, слишком уж много взаимосвязей.
Способ подойдёт, если:
у вас очень компактная база и не нужно хранить много тегов;
ваши тесты имеют смысл именно с реальными данными;
в боевой базе данных нет конфиденциальных данных или вы готовы их маскировать;
вы можете аккуратно вырезать кусок данных и будете в нём уверены.
Размер нашей БД с картографическими данными — несколько терабайт. Представим, что случайная машина в кластере K8s пытается скачать такой объём. Получится не быстро ?
Если у вас большая тестовая база, то вряд ли будет время гонять с ней тесты после каждого коммита. Повод задуматься про прогоны по расписанию.
Если вы копируете для тестовых нужд содержимое боевой БД, аккуратно обращайтесь с конфиденциальными данными. Наверняка у вас есть корпоративные стандарты по защите персональных данных и т. п. Распространённые подходы — это очистка или маскирование. Это всё дополнительные действия при подготовке вашего образа.
4. Растим синтетику
Можно накатить на базовый образ только схему данных. Используете инициирующие миграции или сгенерируете скрипты, при билде образа один раз зададите нужные настройки сервера, а затем будете генерировать тестовые данные из кода.
В итоге образ будет лёгкий, и вы сможете автоматизировать процесс его актуализации.
Способ подойдет, если:
у вас специфические или сложно связанные данные, которые вы не хотите создавать в тестах;
схема данных или используемые сборки и плагины часто меняются.
Вопросы на засыпку
Учитывайте специфику своей системы. В выборе вашей стратегии помогут следующие вопросы:
Какие данные нужны в образе? Какие создавать из кода?
Как ваши данные связаны между собой? В каком порядке создаются?
В каких ситуациях изменяются данные? Какие есть источники изменений?
Как сделали мы
Если вам показалось, что можно сделать образ без данных, а потом добавить сами данные — вам не показалось. Для основной базы данных MSSQL с картографическими данными мы так и сделали.
Получилось два образа. Первый — с настройками сервера и голой схемой. Он не используется в тестах, но служит базой для второго образа. Во второй мы добавили вспомогательные данные (например, геометрию Российской Федерации из 85 тыс. точек), чтобы проще генерировать в тестах все остальное.
В общем, растим синтетику — берём базовый образ и выращиваем вокруг него новые слои.
Само собой, образы приходилось дорабатывать. Мы находили баги — например, то самое отображение арабских символов. И добавляли новые фичи типа историчности для таблиц.
Для баз данных Neo4j и PostgreSQL нам не нужны были никакие данные в тестовых образах, и мы выбрали второй подход — образ без данных.
Так выглядит простой докерфайл для графовой БД Neo4j:
FROM neo4j:3.5.25
ENV NEO4J_AUTH='none' \
NEO4J_apoc_export_file_enabled='true' \
NEO4J_apoc_import_file_enabled='true' \
NEO4J_apoc_import_file_use__neo4j__config='true' \
NEO4J_apoc_trigger_enabled='true' \
NEO4J_dbms_security_procedures_unrestricted='apoc.*, algo.*, splitter.*, gds.*' \
NEO4J_dbms_security_procedures_whitelist='apoc.*,algo.*,splitter.*,gds.*'
COPY apoc-3.5.0.14-all.jar /var/lib/neo4j/plugins/
COPY neo4j-graph-data-science-1.1.6-standalone.jar /var/lib/neo4j/plugins/
COPY node-splitter-1.0.3-SNAPSHOT.jar /var/lib/neo4j/plugins/
CMD ["neo4j"]
Переводя на человеческий язык:
взяли базовый образ → настроили нужные переменные окружения → копировали внутрь контейнера необходимые плагины → запустили Neo4j.
Best practices по написанию докерфайлов
Новые сервисы Fiji делаем сразу с инициирующими миграциями, и это значительно упрощает подготовку инфраструктуры для интеграционных тестов.
Актуализация образов
Всегда гоняйте тесты на актуальном образе. Для этого вовсе не обязательно постоянно плодить новые теги. Мы, например, просто накатываем свежие миграции, когда поднимаем контейнер для прогона тестов. Если разработчик написал новую — её тоже накатим. И в тестах будет всегда актуальный образ.
Пробежимся по трём стратегиям актуализации.
Ничего не делать
Подойдет вам, если:
не меняются схема данных и сами данные;
вы создаете всё нужное из кода.
Но если эти пункты — не про вас, то образ устареет и станет малополезен.
Последовательная стратегия
Берем готовый образ, накатываем патч, получаем новый образ. Это суперпросто — именно так вы делаете с вашим боем. Но есть один нюанс — onion filesystem.
Каждый новый тег — это новый слой над предыдущим тегом. А значит, каждый новый тег будет тяжелее предыдущего (что важно как минимум при первом скачивании рандомной машинкой в кластере K8s) и наследует все его проблемы. Конечно, их можно исправить миграциями, но тогда у вас будут разные наборы миграций на тесте и на бою.
Стратегия пересоздания
Давайте не будем брать предыдущий тег и накатывать на него патч. Мы можем взять одну точку во времени и накатить сразу все патчи, которые вышли с этого момента. Да, актуализация образа будет не быстрой, но зато все патчи уложатся в один слой.
Мы выбрали этот вариант. Он вдвойне хорошо работает, если тестируемый сервис сам знает, какие миграции ему нужны, и умеет их накатывать.
Советы и подводные камни
Автоматизируйте актуализацию образа. Пусть это будет одна кнопка на билд-сервере, а не многочасовой ручной процесс, который так и хочется отложить.
Определите, как часто вам требуется актуализировать образ. В вакууме частая актуализация проще, чем редкая. Тут как с релизами: если выпускать продукт раз в год, набирается слишком много подводных камней.
Настройте процесс очистки старых докер-образов в вашем реестре, чтобы случайно не распухнуть ?
Если внутри тестовой БД есть какие-то данные, задумайтесь: на бою пользователи могут их изменять? Если да, тестовая и боевая БД могут со временем разойтись разными путями. И если на тестовой БД ваши тесты проходят без проблем, то при эксплуатации приложения в боевых условиях могут возникнуть неприятные сюрпризы. Чтобы этого не допустить, можно при актуализации образа импортировать такие данные с боя.
Проект будет меняться. Если когда-то мы использовали Flyway для наката миграций, то теперь сами сервисы Fiji накатывают их через Entity Framework. Закладывайте время на то, чтобы подтягивать актуализацию образов под новые реалии.
Куда нас привели автотесты
3 года назад в проекте Fiji было почти сплошное ручное тестирование, длительный поиск регрессий и редкие тяжелые релизы. Что изменилось?
Мы перешли на быстрые релизы и больше не делаем полного регрессионного тестирования (и очень этому рады ?). Если master зеленый, релизиться можем хоть каждый день.
Мы пишем интеграционные тесты. Причём не только тестировщики, но и разработчики. Иногда тестеры подключаются к код-ревью задач и прямо там накидывают идей на покрытие автотестами. За 3 года количество интеграционных тестов увеличилось в 6 раз.
Мы доверяем автотестам. Они играют определяющую роль в тестировании новых фич: когда критичные интеграции покрыты — релизы проходят спокойно.
Мы значительно сократили время тестирования определённых задач. Самый яркий пример — автораспознавание дорожных знаков. Раньше, чтобы проверить кейсы по автораспознаванию, нужно было находить видео с регистратора под каждый тестовый случай. Теперь мы в коде создаем детекции знаков из воздуха и спокойно проверяем всю бизнес-логику. Это бесценно в случаях, когда в каком-нибудь городе есть всего пара экземпляров редкого дорожного знака.
Длительность билда с прогоном всех тестов на TeamCity постепенно подобралась к двум часам, и это совсем не весело. Сейчас мы почти каждый спринт берём техдолговые задачи на ревизию тестов, распараллеливание или оптимизацию.
Новые сотрудники быстрее готовят рабочее место — всё, что нужно для автотестов, будет сгенерировано прямо во время прогона.
Мы храним описания автотестов только в коде. Благодаря Allure после каждого прогона на TeamCity формируются аккуратные отчёты с описаниями на русском языке, взятые из NUnit атрибута Description. Этот же подход мы распространили на юнит-тесты — документируем их и лучше понимаем, что происходит внутри.
Вызовы, которые стоят перед нами сейчас:
Значительно ускорить и по возможности распараллелить функциональные тесты.
Доработать механизм информирования об упавших тестах, чтобы оперативно замечать падения, агрегировать информацию или выявлять flaky-тесты.
Подключить анализатор покрытия кода и ввести плановый процесс написания тестов на наиболее незащищенный функционал
Если всё пойдет по плану, вы прочитаете об этом в будущих статьях ?
Проблемы → возможности
3 года назад мы рискнули переизобрести инфраструктуру интеграционных тестов на проекте — и это было правильное решение. Да, наш путь был тернист, но мы многому научились. Напоследок дам ещё несколько советов для тех, кто тоже решится пойти по этому пути.
Найдите правильное время, чтобы менять тестовую инфраструктуру. Чтобы воскресить интеграционные тесты, двухнедельного спринта может не хватить. Мы занимались этим, когда на проекте было два тестера и шесть разработчиков. Но совпало так, что почти все разработчики были заняты мегафичами, и мы нашли время на эксперименты. Ловите момент.
Если в вашем проекте есть старые тесты, поймите, что с ними делать. Вы готовы их выкинуть? Или адаптировать под новую инфраструктуру? Если второе, то заложите на это время в зависимости от количества тестов и сложности переезда.
Ваш проект будет развиваться, и тестовая инфраструктура должна за ним поспевать. Например, рано или поздно потребуется масштабировать систему. Если сейчас для автотестов хватает одного контейнера, то через полгода тестовый контур вырастет. Лучше, если код будет к этому готов.
Придумайте не только техническое решение, но и сам процесс написания тестов в вашей команде. Мало сделать инфраструктуру. Сама по себе она не напишет вам тесты. Если на вашем проекте их долго не писали, внедрение — непростая задача. Вам нужно выстроить процесс, который будет работать именно в вашей команде.
P. S. Решайтесь — обратно можно вернуться всегда. Ну и не забывайте про техдолг ?