Подход к автоматизации тестирования CI
Всем привет! Я Лена, инженер по автоматизации тестирования в Т-Банке. Поделюсь нашим опытом в разработке подхода к тестированию, который мы применили к одной особенной сущности — к CI. Расскажу, какие техники используем и как наладили процесс тестирования новых изменений.
Выбираем, что тестировать
В нашем отделе занимаются разработкой неавторизованной зоны сайта Т-Банка, которая предоставляет функциональности для приобретения карт, оформления страхования, мобайла и многого другого.
Неавторизованная зона сайта — это набор независимых приложений для бизнес-продуктов. Каждое приложение находится в отдельном репозитории и имеет собственный релизный цикл, независимый от других приложений. Все приложения типовые: они базируются на едином git flow и проходят одинаковый набор шагов в процессе CI/CD.
Для настройки процессов CI/CD мы используем GitLab. Учитывая то, что большая часть YAML-кода для CI/CD наших приложений схожа, мы решили переиспользовать код. Для удобства построения CI мы вынесли общие файлы конфигурации в отдельный репозиторий и импортируем их в конечные приложения.
Например, вот так в общем репозитории выглядит файл dynamic-stands.yml, в котором хранится джоба деплоя приложения на динамический стенд:
Dynamic_deploy:
stage: stand_generate_specs
allow_failure: true
extends:
- .unic_common
needs:
- job: build_app
artifacts: true
- build_docker_dynamic
before_script:
- *check_branch_name_length
- check_branch_name_length
script:
- unic deploy --app=$APP_ID --stand=$UNIC_STAND --dockerImage=$DOCKER_IMAGE
environment:
name: dyn/${CI_COMMIT_REF_SLUG}
url: $DYNAMIC_URL
on_stop: Delete_dynamic
В репозиторий своих приложений команды просто импортируют файл с этой джобой:
include:
- project: ded-pwa/pwa-ci
file:
- /gitlab-ci/dynamic-stands.yml
У нашего подхода к управлению общими файлами конфигурации есть несколько преимуществ:
Проще поддержка: общие файлы конфигурации хранятся и поддерживаются в одном месте, что позволяет легко обновлять их без необходимости вносить изменения в каждое приложение отдельно.
Стандартизация: использование общих файлов конфигурации фиксирует и поддерживает общие стандарты во всех наших приложениях.
Улучшение эффективности работы: переиспользование сокращает время, затрачиваемое на настройку и управление процессом CI/CD на стороне команд приложений.
У этих плюсов есть обратная сторона: неудачные изменения сломают CI/CD сразу всем командам, использующим файлы из общего репозитория. Например, разработчик внес изменение в общий CI-репозиторий, оно автоматически применилось к CI/CD всех приложений. Команды в своих репозиториях хотят запустить CI/CD и узнают о том, что CI сломан. В итоге процесс релиза откладывается, а разработчик экстренно делает фикс в общем репозитории с CI.
Столкнувшись на практике с такими инцидентами, мы поняли, что изменения в CI нужно тестировать. Перед написанием тест-кейсов и автоматизацией нужно определить, что тестировать, — это и стало первой сложностью.
В нашем общем репозитории мы храним множество различных сущностей: готовые джобы GitLab, bash-скрипты для выполнения общих действий, docker-образы, необходимые для автотестов. Некоторые из этих сущностей могут быть связаны друг с другом: например, джобы могут использовать скрипты, наследоваться друг от друга или требовать наличия других джоб в своих зависимостях.
Начать я решила с написания тест-плана.
Исходим из кейсов использования
Мне нужно было понять, как тестировать систему, которую нельзя пощупать и цель которой — обеспечение инфраструктуры для создания других систем. Я решила построить тест-план. Я оттолкнулась от кейсов использования функциональности из репозитория для хранения CI. Помогло построение схем всех пайплайнов в репах приложений.
Пример схемы пайплайна с указанием джоб и связей между ними
С помощью схем пайплайнов мне удалось понять, что из репозитория для хранения CI используется в реальных приложениях, а что нет. И сразу отсеять тест-кейсы с устаревшей функциональностью. А еще я проанализировала функции каждой джобы и ее связь с другими джобами. На основе выполняемой функциональности определила критичность ее проверки.
Например, есть джобы, отвечающие за деплой новой версии приложения на стенды (тестовые и прод), а есть джобы для нотификации о статусе пайплайна в чаты команд. Первый тип джоб нужно тестировать обязательно, а тестированием второго типа можно пренебречь без угрозы для бизнеса.
В итоге у меня появился набор тест-кейсов, которые стоит проверять при внесении изменений в эту репу. Их оказалось достаточно много — около 50, вот часть из них.
Следующий шаг — сократить количество проверок.
Используем техники тест-дизайна
После первого шага стало понятно, что проверить все поставляемые тестируемым репозиторием джобы невозможно: их слишком много. Посмотрев на тест-кейсы, я поняла, что часть из них очень похожа: например, деплой приложения на разные стенды.
А еще я поняла, что для формирования тест-кейсов нужно учесть не только сценарии использования, но и внутреннюю структуру репозитория. Если эти сценарии реализованы через одну и ту же функциональность, это поможет сократить количество тест-кейсов. Поэтому моим следующим шагом стал анализ структуры репозитория. Я построила схему связей функциональностей в репозитории, чтобы понять, как они связаны друг с другом.
Схема построена с помощью mermaid. Зеленым выделены шаблоны джоб, сами они не исполняются, но используются для наследования в других джобах. Красными выделены исполняемые джобы — в красном квадрате, наиболее приоритетные, использующиеся в наших продуктовых репозиториях
Анализ структуры помогает:
Понять, какие сущности являются ключевыми — это те, к которым идет больше всего стрелочек.
Выявить сущности, которые используются реже всего, и подумать о необходимости их существования. Так мы нашли несколько джоб, которые используются только в одной репе, — логично переместить их туда, а не хранить в общей репе.
Понять, как сущности наследуются друг от друга. Это поможет нам при сокращении тест-кейсов, потому что теперь мы можем применить одну из самых известных техник тест-дизайна — классы эквивалентности.
Остановимся на третьем пункте. Например, есть набор джоб, предназначенных для одной и той же цели — выгрузки файлов исходников приложения в облачное хранилище. Почему тогда это разные джобы? Они запускаются в разных случаях: одна — в MR, другая — на ветке master, а третья — на ветке prod. Не зная внутренней структуры, я бы, скорее всего, пришла к выводу, что нужно тестировать все джобы. Но диаграмма подсказывает: все джобы наследуются от одного шаблона. В нем указана вся основная логика, и различаются джобы только правилами запуска.
Вспоминаем про технику классов эквивалентности в тестировании — все эти джобы относятся к одному классу эквивалентности, а значит, достаточно проверить всего одну джобу из этого класса и отдельно убедиться, что правила запуска работают корректно. Это гарантирует, что тестируемая джоба тоже работает корректно. Используя такой подход, я сократила набор проверок до адекватного количества.
Исходим из проблем
Если полученное мной покрытие полное, составленные тест-кейсы должны отловить проблемы, которые были внесены в репу для хранения CI и сломали CI приложений.
У нас в команде принято заводить в Jira все баги, даже если это не дошедшие до пользователя проблемы, а наши внутренние поломки инструментов. Проанализировав баги в репе для хранения CI за последние полгода, я нашла еще несколько кейсов, которые стоит проверять:
Синтаксис всех YML-файлов. Я решила не тестировать все джобы, потому что полное тестирование избыточно. Но периодически возникает кейс, когда ошибка в синтаксисе YML-файла ломает CI, потому что GitLab не может собрать пайплайн из-за нее. Поэтому синтаксис проверять нужно для всех поставляемых YML-файлов.
Синтаксис всех bash-скриптов. Аналогичная ситуация: некорректный синтаксис может сломать джобы, которые было решено не тестировать.
Логика функций. Bash-скрипты в репе достаточно сложные и порой используют внутри JS-функции. Логику функций можно покрыть юнитами, чтобы, запуская джобы, не тестировать правильность написанных функций.
Применив техники тест-дизайна и проведя анализ проблем, я составила набор тест-кейсов и разделила их по приоритетам, где Critical — то, что сломает релизный процесс приложений полностью.
Примеры из набора тест-кейсов
Настраиваем процесс тестирования
Тест-план готов, осталось наладить процесс тестирования всех новых изменений. Часть проверок можно выполнить вручную: в GitLab есть механизм указания ссылок на ветку, из которой подключается джоба. По умолчанию это master, но можно указать другую. Выглядит это так:
include:
- project: ded-pwa/pwa-ci
file:
- /gitlab-ci/dynamic-stands.yml
file: branch-name
Мы можем подтянуть изменения из нашей ветки в репе с общим CI в репозиторий какого-то из наших приложений и убедиться, что CI/CD приложения не сломался. Есть минусы:
Очень долго. Ручное подключение новой функциональности и ручная проверка работоспособности CI/CD занимают очень много времени тестировщиков.
Нельзя проверить все кейсы. Например, внесли изменение в джобу деплоя на прод — чтобы убедиться, что она не сломана, надо стриггерить пайплайн деплоя в продакшен, что невозможно для реального приложения в целях тестирования.
Так что не будем останавливаться на достигнутом и перейдем к способам автоматизации
Используем статические анализаторы кода
Для части проверок автоматизация может быть реализована с помощью статических анализаторов кода или стандартных методов проверки кода. Делать это можно прямо в репозитории с общим CI:
Проверка синтаксиса YML-файлов — критичный для нас кейс. Можно проверять с помощью библиотеки yamllint.
Синтаксис bash-скриптов можно проверять с помощью shellcheck.
Логика JS-функций. Можно проверять с помощью юнитов с использованием всем известного Jest.
Создаем песочницу для проверки YML-файлов
С проверкой логики работы и корректности связи YML-файлов все не так просто. Нам нужна песочница, где мы сможем тестировать и проверять версии наших YML-файлов, используемых для CI/CD. Это позволит нам убедиться, что новые версии файлов работают как надо, прежде чем их разворачивать на реальные приложения. Для этого мы создали отдельный репозиторий с простым тестовым приложением, которое будет служить платформой для запуска наших YML-файлов.
В песочнице мы собрали приложение, похожее на реальное, и настроили там CI, аналогичный CI наших реальных приложений. Дальше нужно было настроить окружение, которое позволит тестировать новые версии наших файлов из общего репозитория. Для этого понадобилось несколько фич GitLab:
Переменная APPS_CI_REF — задается в триггере в репозитории с общими джобами, чтобы передать информацию о фича-ветке в репозиторий-песочницу и подключить в него версии джоб из тестируемой ветки.
Использование Multi-project pipelines — они позволяют из репозитория с общими YAML-файлами настроить запуск джоб в репозитории с тестовым приложением. В коде триггера указан репозиторий, в котором будут запущены джобы. Стратегия depend позволяет в исходном пайплайне дождаться окончания выполнения запущенных по триггеру джоб, чтобы нельзя было замержить пайплайн до окончания всех проверок.
test-app-simulation:
stage: test_integration
except:
- tags
- master
variables:
APPS_CI_REF: $CI_COMMIT_REF_NAME
APPS_CI_SLUG: $CI_COMMIT_REF_SLUG
trigger:
strategy: depend
branch: master
project: ded-pwa/pwa-ci-test-app
Мы настроили окружение, в которое подключаем новую версию YML-файлов, имитируем аналогичные реальным приложениям условия запуска: например, merge request, master-ветку и prod-ветку. И после этого проверяем, что все пайплайны проходят успешно.
Проверка настроена автоматически: мы подключаем тестовый пайплайн в репозиторий с нашим общим кодом с помощью механизма GitLab-триггеров. Подробнее о том, как работают триггеры в GitLab:
Наш процесс тестирования CI выглядит так:
Разработчик хочет внести изменения в общий репозиторий CI/CD и создает новую ветку с изменениями.
В ветке запускаются статические проверки из предыдущего шага.
В ветке запускается джоба-триггер, который запускает пайплайн в тестовом репозитории, импортируя в него YML-файлы новой версии.
Джоба ждет завершения пайплайна в тестовом репозитории, если в нем есть ошибки, пайплайн падает и сигнализируют разработчику о необходимости фикса. Успешно пройденный пайплайн гарантирует нам, что новые изменения безопасны и мы можем их раскатывать на все наши приложения.
_____________________
Теперь мы начали относиться к нашему CI как к продукту, который является важной частью процесса разработки, и поняли, что его тоже надо тестировать. В итоге работы мы получили тест-план тестирования общего репозитория для организации CI/CD.
Мы автоматизировали проверки, используя статические инструменты, и создали тестовую песочницу для проверки YML-файлов. Это сократило количество инцидентов, связанных с блокировкой процесса CI/CD, и позволило наладить процессы обеспечения качества в одном из наших критичных инфраструктурных репозиториев.
Поделитесь, если у вас был похожий опыт или остались какие-то вопросы :)