Когда речь заходит про автотесты, первыми на ум приходят проверки для UI, API или для мобильных устройств. Однако автотесты нужны не только для проверки пользовательских сценариев. Они могут решать и менее очевидные, но не менее важные задачи, например проверять работу пайплайнов. Если одни и те же пайплайны используют сотни сервисов и библиотек, любая ошибка в них быстро выходит за пределы одного проекта. У многих команд одновременно могут сломаться сборки, релизы и привычный процесс разработки. В нашем случае такие пайплайны работали примерно для 700 сервисов и более 200 библиотечных репозиториев. Чтобы гарантировать работоспособность пайплайнов, мы пришли к идее покрытия их автотестами.

В статье я расскажу, как мы в Ozon покрывали тестами работу пайплайнов в GitLab CI, какие требования нужно было учесть и как в итоге были устроены end-to-end-тесты для таких сценариев.

Введение, или Как всё устроено

Всем привет! Меня зовут Олег, я ведущий инженер по тестированию в платформенной фронтенд-команде Ozon. Общая задача команды платформы состоит в том, чтобы ускорить процесс разработки и развёртывания проектов в компании. Наша команда (я буду называть её FE) предоставляет различные инструменты для других команд фронтенд-разработки: консольную утилиту для быстрого развёртывания проектов, фреймворк для разработчиков с различными конфигами и настройками пайплайнов и много всего остального. То есть фактически нашими пользователями являются другие фронтенд-разработчики Ozon.

Для начала коротко опишу, как у нас распределена ответственность за пайплайны и где именно находится та часть, которую мы тестируем. У нас в компании есть специальная команда релиз-инженеров (назовём её RE), которая в целом отвечает за работу CI/CD в Ozon и предоставляет пайплайны для разных языков разработки и конфигураций проектов. Такие общедоступные платформенные пайплайны лежат в специальном репозитории, и их используют все разработчики в Ozon. А команда FE отвечает за пайплайны для фронтенд-проектов, которые были построены на платформенных решениях. Эти пайплайны лежат в другом репозитории, уже в нашем пространстве GitLab. Мы предоставляем следующие виды пайплайнов:

  • для проектов сервисов;

  • для микрофронтендов;

  • для библиотек;

  • для библиотек с UI-компонентами.

В каждом из вышеперечисленных пайплайнов стандартные джобы от команды RE, такие как проверка наличия ошибок или создание релиза, дополняются специализированными fe-джобами от нашей команды, уникальными для каждого типа проекта. Примерами таких джоб могут быть сборка проекта, запуск линтеров или тестов.

Общие и фронтенд джобы в пайплайне проекта
Общие и фронтенд джобы в пайплайне проекта

Мир глазами разработчика

Давайте опишем сценарий использования, который совершает наш «клиент» — фронтенд-разработчик Ozon.

  • При инициализации нового проекта он запускает платформенную утилиту и, указывая в ней нужные настройки, разворачивает проект. В нём он сразу получает все готовые конфиги и в том числе настройки пайплайнов.

  • После этого разработчик добавляет в проекте нужную бизнес-логику и отправляет эти изменения в GitLab.

  • В GitLab CI в его проекте выполняются пайплайны, в которых, помимо прочего, отрабатывает логика наших платформенных fe-джоб.

В целом звучит несложно. А теперь это надо протестировать.

И как это тестировать?

Итак, у нас есть следующие вводные:

  • хочется протестировать работу fe-джоб на реальных пайплайнах, чтобы убедиться, что всё работает как ожидалось;

  • нужно запускать тесты из нашего платформенного репозитория с fe-джобами на свежих изменениях, чтобы убедиться, что они ничего не сломали;

  • пользователи не используют напрямую платформенный fe-пайплайн — он подключается внутрь пайплайна от команды RE.

Подключение пайплайнов друг в друга
Подключение пайплайнов друг в друга

Пройдёмся по всем этим пунктам.

Запуск тестов в продовом окружении

Продовым окружением в данном случае будет обычный проект в GitLab, в котором будут запускаться пайплайны. По сути, это то, как выглядит стандартная разработка на проектах, — создание feature-веток, создание релизов, деплой в различные окружения и т. д. А тесты будут представлять собой end-to-end-сценарии на проверку логики работы джоб.

Запуск тестов на свежих изменениях

При внесении изменений в репозиторий с платформенными фронтенд-пайплайнами нам необходимо проверить, что на текущих изменениях всё работает корректно. Для этого мы должны встроить тесты в пайплайн нашего fe-проекта и запускать их на той же ветке.

Способ подключения пайплайнов

Как я писал выше, разработчики не используют напрямую платформенный fe-пайплайн, так как он является составной частью итогового общедоступного пайплайна от команды RE.

# pub/ci/.fe.gitlab-ci.yml

include:
  - local: ".common.gitlab-ci.yml"
  - project: 'fe/pipeline'
    ref: 'master'
    file: '/templates/fe.yml'

Выше приведён пример того, как в итоговый файл пайплайна подключаются два пайплайна: базовый пайплайн .common.gitlab-ci.yml от команды RE и наш fe-пайплайн. Как можно видеть, в параметре ref указана ветка master. Однако нам нужно будет использовать текущую feature-ветку в проекте fe/pipeline, на которой мы и хотим запустить тесты.

Дело ещё осложняется тем, что результирующий файл пайплайна находится в другом репозитории, и это приводит к следующим моментам:

  1. За этот репозиторий отвечает другая команда, в которой есть свои регламенты по внесению изменений.

  2. Чтобы изменить ветку с master на feature-ветку, нам придётся создавать тестовые ветки в чужом репозитории, чего лучше избегать.

  3. Результирующий пайплайн содержит дополнительные джобы с проверками качества кода и покрытия тестами, которые в нашем случае избыточны и будут нам только мешать при прохождении тестов.

Чтобы избежать всех этих сложностей, было решено не использовать в тестах напрямую результирующий пайплайн от команды RE, а брать только нашу часть из fe-пайплайна и точечно дополнять её до минимального рабочего состояния. Таким образом, мы будем полностью управлять тем, что попадёт в пайплайн в тестовом проекте, и избежим ненужных проверок и лишних джоб.

Как устроены тесты

Ниже я постараюсь описать, что вообще собой представляют такие тесты пайплайнов, как они работают и какие особенности нужно учитывать.

Запуск тестов

Данные тесты написаны на языке Typescript, они хранятся в проекте с фронтенд-пайплайнами fe/pipeline и запускаются в нём же набором джоб. Каждая такая джоба запускает скрипт со своим тестом. Так как нужно проверить разные типы пайплайнов (для проектов, библиотек и т. д.), то под каждый случай создан отдельный скрипт и отдельная джоба по запуску.

Джобы по запуску тестов на пайплайны
Джобы по запуску тестов на пайплайны

Тестовые репозитории для выполнения пайплайнов

Каждый тест разворачивает проект, подключает в нём нужный тип пайплайна и выполняет свой набор действий и проверок. Чтобы разграничить между собой тесты и сделать возможным их независимую работу, под каждый тип теста заранее были созданы репозитории в GitLab. Каждый такой репозиторий по умолчанию пустой и нужен только для того, чтобы в нём выполнялись пайплайны, созданные в рамках теста.

Схема работы тестовой джобы
Схема работы тестовой джобы

Особенности выполнения тестов

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

Стандартная разработка в Ozon выглядит так:

  • создание feature-ветки и соответствующего ей пайплайна;

  • создание релизной ветки и релизного пайплайна;

  • создание тегового пайплайна;

  • merge в master-ветку;

  • merge в develop-ветку.

Каждый тест проходит весь сквозной цикл работы пайплайнов, начиная с feature-веток и заканчивая пайплайном с мержем в master-ветку. При таком выполнении в рамках тестового репозитория не могут одновременно работать несколько тестов, так как они будут влиять на работу друг друга и мешать выполнению шагов и проверок. Какой отсюда вывод? Так как для каждого теста подготовлен отдельный тестовый репозиторий, нужно перед началом теста проверять, не занят ли этот репозиторий каким-то другим тестом. Такой подход позволяет всем тестам на пайплайны работать параллельно друг другу, тем самым ускоряя общее время прохождения тестов.

Проверка доступности репозитория

Для того чтобы проверить, не выполняется ли сейчас в тестовом репозитории какой-то тест из другой ветки, мы используем специальную переменную FE_PIPELINE_TESTS_LOCK, которую добавляем в GitLab Variables тестового репозитория. Изначально эта переменная пустая, важно лишь её наличие в проекте.

Когда тест начинает своё выполнение в репозитории, он записывает в переменную сгенерированное значение — id джобы с тестом и текущий timestamp. Перед тем как тест будет запущен, он получает значение данной переменной и проверяет его. Если переменная непустая и с момента последней записи прошло не больше максимального допустимого времени (оно задаётся в тестах), то это означает, что сейчас выполняется другой тест и репозиторий занят. В таком случае тест завершится с ошибкой.

Таким образом мы гарантируем, что в один момент времени в репозитории будет выполняться только один тест и его проверки не будут задеты соседним тестом.

Проверка и установка блокировки на тестовый репозиторий
Проверка и установка блокировки на тестовый репозиторий

Снятие блокировки с репозитория

В конце каждого теста нам необходимо снять блокировку с репозитория, то есть стереть выставленное значение из переменной GitLab FE_PIPELINE_TESTS_LOCK, чтобы другие тесты могли быть запущены. Однако ещё может быть ситуация, когда в результате какой-то ошибки либо при ручной остановке джобы тест прекратил своё выполнение, но блокировка с репозитория не была снята. Чтобы другие тесты не были заблокированы и нам не пришлось менять значение переменной FE_PIPELINE_TESTS_LOCK вручную, были добавлены отдельные джобы для снятия блокировок с тестовых репозиториев.

Джобы для снятия блокировок с репозиториев
Джобы для снятия блокировок с репозиториев

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

Подготовка проекта

Каждый тест вначале создаёт временную директорию, в которой будет настроено содержимое проекта. Так как платформенная команда FE предоставляет разработчикам утилиту для быстрого развёртывания проектов, в тестах мы используем непосредственно её. В конфигурационном файле указываем, какие настройки должны быть переданы в утилиту, и в зависимости от этих настроек в проект будет добавлен определённый набор файлов. Например, если утилита была запущена с настройкой для обычного проекта, будет добавлен пайплайн для сервисов, если с настройкой для библиотеки — будет добавлен пайплайн для библиотек и т. д. Таким образом, в зависимости от проверяемого сценария в проекте будет сформирован различный gitlab-ci.yml и, соответственно, будет различаться логика работы пайплайнов.

Альтернативным способом для подобной настройки проекта может быть хранение в репозитории с тестами файлов (или их создание из кода тестов), которые будут копироваться во временную директорию.

Также мы модифицируем файл gitlab-ci.yml — подключаем базовый платформенный пайплайн от команды RE, а в нашем fe-пайплайне заменяем ветку с master на текущую ветку, с которой запускаются тесты:

// Модификация файла пайплайна в тесте

const parsed = YAML.parseDocument(content)

parsed.setIn(['include'], [
	{
		project: 'pub/ci',
		ref: '0.0.6',
		file: '.common.gitlab-ci.yml', // базовый платформенный пайплайн
	},
	{
		project: 'fe/pipeline',
		ref: pipelineRef, // текущая ветка
		file: 'templates/fe.yml', // тестируемый fe-пайплайн
	},
	...(parsed.toJS().include).slice(1),
])

После того как файлы проекта сформированы, в созданной директории необходимо инициализировать git-репозиторий, создать ветку, добавить все файлы в индекс и отправить в remote-репозиторий (это как раз тот тестовый репозиторий, который был создан заранее).

Способы формирования содержимого тестового проекта
Способы формирования содержимого тестового проекта

После этого в тестовом репозитории будет автоматически запущен пайплайн, с которым начнёт работу код теста.

Откат до начального коммита

Как видно из описания, наши тесты модифицируют тестовый репозиторий, в котором разворачивается проект: создают в нём ветки и делают коммиты. Но для стабильной работы тестов необходимо, чтобы предыдущее их выполнение не оказывало влияния на последующие проверки. 

Для этого в тестах хранится хеш Initial-коммита для каждого тестового репозитория. Перед началом теста выполняется принудительный откат к данному коммиту, что очищает репозиторий до начального состояния. Это позволяет каждому тесту выполняться с чистого листа, как если бы никаких других тестов перед этим не было запущено.

Сторонние библиотеки и собственные хелперы

Для работы с GitLab в тестах мы подключаем одну из внешних библиотек, которая использует GItLab REST API. Данная библиотека предоставляет базовые методы по взаимодействию с пайплайнами, джобами, артефактами и т. д.

Однако чтобы сделать работу с тестами проще и надёжнее, нам пришлось написать собственные функции-хелперы поверх данной библиотеки. Примерами таких хелперов могут быть поиск пайплайна по заданному условию, запуск джобы по её имени и т. д.

// Пример хелпера для поиска релизного пайплайна

/**
 * Найти и дождаться завершения пайплайна на релизной ветке
 */
export const findAndWaitReleasePipeByTag = track(async function findAndWaitReleasePipeByTag({
	tag,
	access,
}: {
	tag: string
	access: ApiAccess
}) {
	const releaseBranch = `release/${tag}`
	const releaseSha = await getLatestCommit(releaseBranch, { access })
	const releasePipeId = await findAndWaitPipe({
		sha: releaseSha,
		branch: releaseBranch,
		access,
	})

	return {
		sha: releaseSha,
		pipeId: releasePipeId,
		branchName: releaseBranch,
	}
})

Отсутствие готового фреймворка для тестов

Как, наверное, стало понятно из всего вышенаписанного, данная задача по тестированию не является типовой. Тестовые фреймворки вида vitest или playwright в данном случае не подходят, так как каждый наш тест представляет собой одну большую последовательность действий с подготовкой проекта, ожиданием пайплайнов и джоб, выполнением проверок. Такие шаги нельзя запускать параллельно. Использовать стандартные библиотеки с проверками тоже не всегда удобно. В данном случае мы выносили все проверки в отдельный набор методов, внутри которых проверяли полученные данные и при несовпадении с ожидаемым поведением пробрасывали исключение. В таком случае джоба с запущенным тестом будет завершена с ошибкой.

Отдельно хочется упомянуть про важность и подробность логирования, которые здесь необходимы. Так как весь ход теста идёт внутри джобы и включает в себя как настройку проекта, взаимодействие с GitLab API, так и разнообразные проверки, то требуется подробно фиксировать все этапы и обрабатываемые в тесте данные. Так отладка тестов и поиск проблем становятся намного проще :)

Логи джобы при выполнении теста
Логи джобы при выполнении теста

Что проверяют тесты

Проверки сильно зависят от того, какой тип пайплайна используется в тестовом проекте.

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

  • В пайплайнах для библиотек проверялось наличие или отсутствие джоб для определённых веток, логи и артефакты платформенных джоб, отвечающих за публикацию пакета в artifactory.

  • В пайплайнах для микрофронтендов мы также проверяли работу с несколькими релизами, данные файла контрактов микроприложений и доступность платформенных переменных окружения.

Так как каждый тест выполняет проверки на нескольких ветках (для develop-пайплайна, release-пайплайна и т. д.), то его прохождение требует ощутимого количества времени. Наши тесты работают в диапазоне от 5 до 20 минут каждый. В это время входит очистка тестового репозитория, подготовка проекта, ожидание пайплайнов и джоб, выполнение проверок. Поэтому в end-to-end-тестах была цель проверять только самое необходимое, чтобы оптимизировать время выполнения тестов. В каждом тестовом сценарии был выбран набор критичных джоб, которые требуется проверить детально. Для таких джоб в тестах мы валидировали их логи, артефакты и делали проверки, специфичные под каждый случай. Для менее критичных джоб мы ограничивались проверкой статуса их выполнения в пайплайне.

// Фрагмент теста c проверкой fe-джобы для пайплайна библиотеки

// Запустить джобу 'fe npm canary' на релизном пайплайне и дождаться её завершения
const feNpmCanaryJobId = await runFeNpmCanary({
	pipeId: release.pipeId,
	access,
})

// Проверить логи джобы 'fe npm canary'
const canaryVersion = await checkFeCanaryLogs({
	jobId: feNpmCanaryJobId,
	pkg: {
		name: packageName,
		version: expectedCanaryVersion,
	},
	branchName: release.branchName,
	access,
})

// Проверить, что у джобы 'fe npm canary' в артефактах есть файл published.json,
// проверить содержимое файла published.json
await checkFeNpmCanaryArtifacts({
	jobId: feNpmCanaryJobId,
	packageName,
	version: canaryVersion,
	access,
})

// Проверить добавление пакета в артифактори
await expectPackageToBePublished(packageName, canaryVersion)

Общая схема работы тестов

Ниже приведена общая схема выполнения тестов на пайплайны (с некоторыми упрощениями).

Общая схема работы тестов
Общая схема работы тестов

Заключение

В итоге мы собрали рабочий подход к автотестированию пайплайнов GitLab. С его помощью мы регулярно прогоняем тесты, когда какие-либо изменения вносятся в репозиторий с платформенными фронтенд-пайплайнами. С одной стороны, это позволяет нам обеспечивать стабильность работы платформенных решений, которыми пользуются многие команды разработки в Ozon. А с другой стороны, экономит время на ручное выполнение подобных проверок, которые занимали несколько часов у разработчиков, теперь они выполняются в 10 раз быстрее. 

Итак, на что стоит обращать внимание при разработке похожих автотестов:

  • Время работы. Подготовка тестовых данных, выполнение и проверка джоб требуют времени, поэтому важно сформировать оптимальные тестовые сценарии и избегать лишних действий и проверок. 

  • Управление состоянием тестовых репозиториев. Следует предусмотреть возможность отката до эталонного состояния перед выполнением теста, а также выполнять удаление лишних тестовых артефактов — созданных веток git, деплоев и т. д.

  • Тестовая инфраструктура. Не всегда получится использовать тестовые фреймворки и библиотеки «из коробки», особенно когда дело касается специфических областей применения. Написание удобных вспомогательных методов позволит сделать тесты читаемыми и легко поддерживаемыми.

  • Подробное логирование. Для более простой отладки тестов и нахождения причины их падения нужно логировать все выполняемые действия и шаги теста. Особенно это становится критично в ситуации, когда не используются стандартные тестовые фреймворки.

Надеюсь, эта статья помогла пролить свет на не самые частые области тестирования. Спасибо за чтение. Буду рад вашим комментариям.

Комментарии (0)