Меня зовут Артём Дружляков, я техлид QA в направлении кредитования. Сегодня я хочу рассказать о проекте, который мы реализовали в направлении тестирования микросервисов в Альфа-Банке, — о разделении автотестов по микросервисам.
Раньше все автотесты жили в одном огромном репозитории — больше 550 микросервисов в одном месте. Из-за этого сборки шли долго, проект был нестабилен, любое изменение в общем файле могло сломать часть тестов или запуски всех, а новичкам было сложно разобраться в громоздкой архитектуре.
Чтобы решить эти проблемы, команда перешла на микросервисный формат. Теперь автотесты на каждый микросервис хранятся в отдельном репозитории, который создаётся автоматически с помощью шаблонизатора и уже содержит все необходимые файлы. Можно использовать генерацию по спецификации OpenAPI, а общие решения вынесены в отдельные библиотеки, покрытые юнит-тестами и подключаемые при необходимости.
Примечание. Этот материал дополняет статья «Как преобразовать огромный монорепозиторий с автотестами в микросервисы», которая рассказывает о той же проблеме, решенной несколько иначе. |
О проблематике
Раньше мы все жили в одном большом проекте с нашими API-тестами. В какой-то момент зафиксировали, что тестировщикам стало слишком тяжело работать с проектом. Скорее всего проблема была в размере проекта — всё-таки 1,5 ГБ, даже локально его скачать с нуля занимает минут 10-20.
Размер проекта тянет за собой проблему локальных сборок: тестировщик пишет тест, ждёт 2-3 минуты пока все скомпилируется и запустится. Видит, что ошибся в ассертах, правит и заново ждет компиляцию.
И так по кругу.
Та же проблема наблюдается в CI/CD, только время сборки там еще дольше — в среднем 20-30 минут. А сборка нам нужна для проверки целостности кода — проверяется компиляция, чекстайл.
Следовательно, такое долгое время сборки увеличивает время проходения merge-request. Частая история, когда поправить надо одну строчку, из-за очередей на сборку в Jenkins ждать приходится час. Особенно такое поведение раздражало в случае минорного хотфикса для раскатки.
Сам по себе большой проект тяжело рефакторить. Изменения в ядре могут случайно сломать все тесты или часть из них. Внедрение новых подходов может растягиваться, например, переход на обертку для Retrofit так окончательно и не завершился, и в проекте одновременно существовало три подхода к использованию Retrofit. Перевод всего проекта на новые рельсы единомоментно так же сложно осуществим в наших реалиях.
Сама архитектура проекта также вызывала сложности в поддержке и рефакторинге: много цепочек наследования, завязывание сотен классов на один базовый, завязывание всех http-классов для вызовов сервиса на один экземляр Retrofit, единый Spring-контекст на все тесты.
Все описанные проблемы приводили и к трудностям в погружению новичков. Нельзя методом тыка открыть любой тест и со 100% уверенностью сказать — здесь всё актуально.
Немного о код ревью автотестов — его по сути не было. Есть базовые проверки на кодстайл, принятый в проекте, нейминги, комментарии про суть теста — редкость. Шаблонизации, автогенерации также не было.
Решение в разделении
Итогом внутренних обсуждений и пилотных проектов стало решение кардинально изменить архитектуру.
Так на нашем проекте множество микросервисов (API) и много микросервисов дорабатываются несколькими командами, поэтому решили разделять автотесты по принципу «Один репозиторий содержит тесты на один API».
Примечание. Забегая вперед скажу, что такой подход в результате позволил изолировать все зависимости, ответственность, ускорить процессы.
Итак, мы решили разделяться, а как быть и с чего начать? Для успешной реализации этого решения мы сформулировали ключевые требования:
Должна быть шаблонизация генерации репозитория со всеми базовыми классами.
Простота используемых технологий — должен быть простой стек.
Наличие CI/CD для каждого репозитория.
Возможность параллельного запуска тестов одновременно в разных репозиториях.
Общие библиотеки для инструментов и общих классов (для переиспользования).
Наметили план:

Давайте по нему и пойдём.
Подготовка внутренних библиотек
Сперва начали выносить в библиотеки базовую часть.
самые популярные константы,
классы параметры для HTTP-запросов,
общие POJO/DTO-классы,
генерация и конфигурация тестовых пользователей,
и прочее.
По пути сначала совершили ошибку — сделали отдельный репозиторий на каждую библиотеку. В итоге уже на пилотном этапе мы утонули в версиях. После обновления, например, библиотеки для HTTP-параметров надо было обновить версию во всех библиотеках, где она использовалась и потом хранить информацию о актуальных версиях каждой библиотеки.
Чтобы исправить проблему с зоопарком версий библиотек, мы сделали многомодульный проект для библиотек. Каждый модуль предполагается отдельной, изолированной настолько, насколько это возможно, библиотекой. Поддерживать версии наших инструментов стало значительно проще: у всех библиотек одна версия. И при этом мы сохранили возможность подключить атомарную библиотеку как jar-зависимость в проект с тестами.
Дополнительное преимущество проекта с библиотеками — покрытие юнит-тестами и запуск тестов в CI/CD. В старом проекте практически все самописные инструменты у нас не покрывались тестами и у нас не было гарантии, что при рефакторе мы не сломаем что-либо потребителям. Сейчас же прошедшие юнит-тесты являются обязательным артефактом для мержа в мастер и последующей релизной сборки.
Также обновление документации является обязательным требованием для мержа, что тоже упрощает дальнейшее использование.
Шаблонизация: репозитории по «кнопке»
После разработки основных библиотек нужно было реализовать шаблонизатор для генерации репозитория по кнопке.
Для шаблонизатора я выбрал инструмент Lazybones, так как для него на проектах уже есть развернутая инфраструктура.
В шаблоне организовали:
структуру проектов (main часть с тестами),
самые базовые классы,
настройки Gradle,
gitmodule с пайплайном Jenkins,
настройки Allure Report.
Cквозь боль и страдания шаблонизатор был настроен, сейчас тестировщики могут генеририровать репозитории по сути просто по кнопке.

В результате у них получается вот такой небольшой проект.

Примечание. Если решите делать шаблоны на Lazybones, учтите, что сам Lazybones кеширует шаблоны локально и при обновлениях шаблонов и запусков тестов на генерацию самого шаблона могут быть сюрпризы.
Генерация по спецификации open-api
Поскольку мы начали наши микросервисы переводить на open-api, то не могли не воспользоваться и этим — подключили Open Source плагин автогенерации классов по спецификации.
Но возник такой момент: подключение плагина — это достаточно много строк кода в Gradle, не считая того, что требуется поддерживать шаблоны для генерации.
Чтобы у нас была централизованная поддержка шаблонов, решили вынести всю логику взаимодействия с плагином в самописный плагин-обертку над openapi.generator, чем упростили себе жизнь. В итоге у нас вся логика инкапсулирована в нашем плагине, а в проектах с тестами достаточно его подключить и обновлять версию при изменениях.
Это значительно ускорило процесс написания тестов — сразу генерируются все классы с логикой http-запросов, POJO/DTO, заглушки для тестов, в которых тестовый метод генерируется с уже готовыми аннотациями, шаблонами проверок, где тестировщику нужно лишь заполнить конкретные значения, либо дописать логику.
Ранее подключение плагина осуществлялось так (пример из документации):
openApiGenerate {
dependsOn("downloadSpec")
generatorName = "java"
inputSpec = "$inputSpecPath" + "$inputSpecFileName"
outputDir = "$rootDir/generated-sources/openapi"
apiPackage = "ru.alfabank.mobile.api.realty.client.generated"
modelPackage = "ru.alfabank.mobile.api.realty.client.generated.entity"
id = 'api'
templateDir = "$rootDir/src/main/resources/templates/"
skipValidateSpec = true
logToStderr = true
enablePostProcessFile = false
generateModelTests = true
configOptions = [
"dateLibrary": "java8",
"serializationLibrary": "jackson"
]}
После выноса в отдельный плагин так:
classpath "ru.alfabank.alfa_mobile_qa_autotests_utils.plugin:qa-open-api-unirest-generation-plugin:${pluginVersion}"
apply plugin: 'qa-open-api-unirest-generation-plugin'
autotestsGenerationProperties {
specDevHubUrl = "http://artifactory/demo-api/0.0.1/demo-api-0.0.1-specs.zip!/demo-api.yaml"
}
Всё, что нужно для запуска генерации тестов по спецификации, — подключить плагин и указать ссылку на спецификацию из нашего хранилища. И всё, дальше запускается таска для генерации.
Генерируются http-клиенты:

POJO/DTO-классы:

И тесты:

Точнее не тесты, а заглушки, потому что тестировщики должны дописать генерацию тестовых данных, проверки, промежуточные этапы при необходимости.
Стандартизация проверок
Дальше мы стандартизировали написание проверок. Для этого я разработал библиотеку-обёртку над JUnit5, которая позволяет использовать цепочки проверок с понятными описаниями. Каждая проверка автоматически попадает как шаг в TMS. Как это выглядит:
@Test
@AllureId("474312")
@ApiMethod(CUSTOMERS_ME)
@DisplayName("Проверка вызова customers/me")
public void getCustomersInfoTest() {
var response = userInfoApi
.executeCustomersMeRequest(requestHeaders)
.isStatusCodeEquals(200);
expect(response.getBody(), isNotNull(), "Проверка, что тело ответа не null");
var responseBody = response.getBody();
initChecksChain().softChecks()
.expect(responseBody.firstName(), isEquals(user.getName()),
"Проверка, что имя пользователя в ответе API соответствует значению в БД")
.expect(responseBody.lastName(), isEquals(user.getLastName()),
"Проверка, что фамилия пользователя в ответе API соответствует значению в БД")
.expect(responseBody.mobilePhone(), isEquals(user.getPhoneNumber()),
"Проверка, что номер пользователя в ответе API соответствует значению в БД")
.execute();
}
А такой результат получаем:

В итоге в результате прохождения автотеста мы получаем и читаемый результат запуска и читаемый тест кейс (стереотипного кода меньше).
Переиспользование кода автотестов
Для нас было важно дать возможность переиспользовать код между проектами, чтобы не описывать несколько раз одно и то же, потому что часто в тестах на API «А» нужно использовать методы из API «Б».
Мы настроили возможность публикации артефактов main-части репозиторием автотестов в хранилище, что позволило использовать клиенты и вспомогательные классы из одного проекта в другом. Например, автотесты на API «A» могут использовать http-клиенты и инструменты из проекта API «Б», не дублируя логику.
В результате помимо репозитория с автотестами мы получаем ещё и уже скомпилированный jar-артефакт, который мы можем переиспользовать в проектах API и UI тестов. Пример: через implementation подключается такая библиотека и используются в тестах, если нужно что-то вызвать.

Настройка CI/CD
CI/CD мы построили на основе единого пайплайна, подключаемого через gitmodules. Каждый репозиторий имеет свои этапы сборки и запуска, а также возможность запуска тестов. Такой подход дал возможность запустить автотесты на конкретную API из CI/CD по кнопке, попутно это дало удобство разработчикам.
Мы также реализовали общую джобу, позволяющую запускать тесты сразу по нескольким микросервисам. Например, при деплое связанных сервисов или при регулярных запусках я могу запустить сразу все автотест своего направления. Запуск в таком варианте триггерит «маленькие» джобы в параллель, что ускоряет время больших прогонов (если они нужны).
Здесь всё просто. Выбираем TEST_RUN, запускаем тест.

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

При этом можно выбирать как отдельно по репозиторию, так и выбрать команду, и запустятся все репозитории, в которых команда пишет тесты (это всё указывается в настроечном файлике). Если нужно запустить тесты с определенным признаком, например, Smoke — это тоже работает.
В таком формате запуски идут в параллельном режиме. Джоба триггерит маленькие джобы проектов в параллельном режиме.
Интеграция с Test Ops
Все проекты интегрированы с Allure Test Ops. Результаты тестов публикуются централизованно в один общий проект.
Общая джоба запуска умеет собирать результаты в один отчет, что упрощает разбор при запуске автотестов на несколько микросервисов. Также и в обратную сторону — мы можем запускать автотесты в наших проектах через UI-интерфейс Test Ops.
Результаты
Время локальной сборки сократилось с 1-4 минут до 5 секунд (максимум). В зависимости от локального железа прирост в скорости от 25 до 100 раз.
Сборка в CI — с 20–30 минут и выше до 90 секунд. В среднем прирост скорости в 30 раз. Также мы самим архитектурным подходом устранили очереди в Jenkins.
Повысилась устойчивость к поломкам, потому что для любого изменения нужно пройти пулл-реквест, собрать билд, чек-стайлы, SonarQube и т.д.
Чтобы внести изменения в библиотеке нужно, чтобы собрался зеленый билд, чтобы прошли юнит-тест. Проект с библиотеками также покрывается документацией (и тестами), что упрощает работу с ними.
Повысилась скорость доработок автотестов. Нам не нужно ждать по полчаса пока соберется каждый билд: минута, билд зеленый, если к коду нет замечаний — мерж в мастер.
Появилась автогенерация.
Упростили стек и саму архитектуру, репозитории простые, прозрачные, устранены сложные цепочки наследований.
Нам стало проще внедрять изменения — новые подходы и практики мы можем подключать в репозитории постепенно. Например, при попытке подключить SonarCube в старый проект я насчитал несколько тысяч ошибок — править их было бы сложно. Здесь же мы можем постепенно раскатывать изменения, не влияя на соседние репозитории.
Появилась чёткая ответственность за каждый репозиторий.
Упростилась навигация и погружение новых сотрудников.
Разработчикам стало проще жить. Разработчик в может взять джобу, запустить в ней тесты на своей фича ветке (изолированный образ в Kubernetes), если автотесты все прошли или хотя бы smoke-тесты прошли, , то разработчик может передать задачу в тестирование.
Конечно, у подхода есть и сложности. Теперь нужно клонировать несколько репозиториев (справедливости ради, их у нас и так не мало). Нужно поддерживать общие библиотеки и пайплайны. Настроить запуск тестов по всем API стал несколько сложнее,, но на практике он требуется редко — чаще нужны изолированные запуски.
Выводы
Такой подход эффективен, если у вас множество микросервисов и несколько команд работают над разными API.
Критически важно иметь шаблонизацию создания репозиториев.
При наличии OpenAPI — обязательно настройте автогенерацию. Так же Open Api может дать и дополнительные возможности - например, смотреть если эндпоинты из спецификации вообще в автотестах.
Необходим доступ к хранилищу артефактов для публикации и переиспользования библиотек.
Не могу не отметить, что за счет простоты репозиториев, мы получили большее вовлечение QA-инженеров в написание тестов. Мы видим, что если не нужно постоянно ждать долгих сборок, если сама архитектура проекта настолько простая, что понятна даже начинающему QA, если большинство «стандартных» комментариев на код-ревью устранены автогенерацией и расширенным статическим анализом кода - покрытие автотестами растет, у инженеров меньше демотивирующих факторов и это максимально положительным образом влияет на результат.
Телеграм-канал Alfa Digital, где рассказывают о работе в IT и Digital: новости, события, вакансии, полезные советы и мемы.
gigimon
Просто к тестам надо относиться также, как и коду. Если у вас независимые микросервисы, используйте прямо в этом микросервисе независимые от других репо тесты. Если у вас микросервисы используют "общие" библиотеки, используйте в тестах такой же подход.
Не совсем понимаю проблему компиляции (длительности), когда тесты собраны в один репозиторий. Мы выносили просто в отдельную сборку и отдельный docker контейнер. В пайплайне сервиса просто запускали докер контейнер и запускали нужные для этого репозитория тесты. И ровно также, как и проект делали тэги тестов, т.к. репозиторий с тестами ровно такой же компонент системы (один из микросервисов), который также версионируется, релизится и т.п.
art101994 Автор
Проблема компиляции растет из-того, что в проекте работает огромное количество команд, а пайплайн настроен так, что на каждую сборку репо качается с нуля.
Это тянет огромные очереди.
Докер-контейнер, увы, никто не подключил)
Касательно микросервисов и организации тестов на них - полностью согласен, мы так и поступили.