Тестируем будущее: экспериментальный подход к релизам
Привет, Хабр! Меня зовут Матвей, я staff-инженер по автоматизации тестирования в компании Купер (кто это такие — можете почитать в статье). Сегодня хочу остановиться на ключевых трудностях, которые ставит перед тестированием микросервисная архитектура, и рассказать, как мы в компании справляемся с этими вызовами с помощью экспериментальных подходов.
Автоматизация тестирования уже давно стала неотъемлемой частью разработки программного обеспечения, но часто не успевает за ее темпами.
С микросервисами сложностей только прибавилось. Теперь у нас не просто приложение, а целый «зоопарк» независимых сервисов, которые постоянно меняются. Чтобы каждый релиз прошел гладко, нужно не только следить за актуальностью всех сервисов, но и адаптировать тесты к любым обновлениям на лету.
Согласованность обеспечивают контракты между сервисами, которые проверяются контрактным тестированием.
Для проверки системы целиком используют E2E-тесты. Однако в условиях частых релизов их поддержка становится затратной и малоэффективной. А низкоуровневые тесты не дают полной картины.
Давайте вместе разберем ключевые вызовы тестирования в микросервисной архитектуре и обсудим, как экспериментальные подходы помогают справляться с ними.
Микросервисный мир
Ускоренное время выхода на рынок, независимые циклы обновлений, лучшая организационная масштабируемость — такие достоинства микросервисной архитектуры мне выдал ИИ. Однако для полноценного E2E-тестирования все это становится серьезным вызовом.
Что такое «полноценное E2E»
Здесь стоит сразу пояснить, что я подразумеваю под «полноценным E2E»: это когда тесты действительно симулируют действия реального пользователя и проходят все шаги в продукте так, как прошел бы их конечный пользователь.
Привычная автоматизация тестирования зародилась в эпоху монолитных приложений, когда релизы выпускались циклично, а продукт представлял собой единое целое. Тогда был стабильный стенд, настроенный под релизную версию. Цель автоматизации — сократить ручной труд и ускорить регрессионное тестирование, делегируя рутину. Успех измеряли покрытием и скоростью тестов: чем больше автоматизации, тем меньше времени на тестирование.
Со временем продукты разрастались, их поддержка усложнялась, и тогда сами леса Лордерона прошептали его имя: «Микросервис». Так они и появились (ну, или что-то около того). Микросервисы упростили разработку, ускорили релизы, но перевернули автоматизацию с ног на голову. Закручинились тогда автоматизаторы и думу тяжкую задумали.
А в зоопарке есть дрессировщики?
Ключевые проблемы, с которыми сталкиваются команды при работе с микросервисами:
Независимые обновления. Тесты зеленые, контракты соблюдены, юниты пройдены, но в продакшене что-то ломается. Независимые релизы сервисов могут привести к тому, что обновление одного из них нарушает взаимодействие с другим. Даже если тесты проходят успешно, внезапные изменения в зависимом сервисе могут привести к сбоям.
Недостаточное покрытие. Тесты могут проверять отдельные аспекты системы (юниты, контракты, интеграции), но они не охватывают всю бизнес-логику.
Влияние зависимостей. Изменения в одном сервисе могут незаметно повлиять на работу другого. Проверка изолированных модулей не всегда отражает, как изменения скажутся на системе в целом.
Как гарантировать, что все связи между микросервисами и общая логика работы системы останутся целыми, несмотря на частые обновления и сложные зависимости?
Запускать или не запускать, вот в чем вопрос
На первый взгляд, решение кажется простым: после юнит-, контрактных и интеграционных тестов провести классические E2E. Но в мире микросервисов старые подходы перестают так хорошо работать. Почему?
Тестовая среда. Развернуть полноценное окружение, максимально близкое к продакшену, — задача, достойная подвига Геракла. Десятки сервисов, а то и сотни, каждый со своим циклом релиза и обновлениями, координация и поддержание актуальности становятся практически невозможными. Даже если это удается, ресурсы для таких стендов превращаются в серьезную статью расходов. Тестировать отдельные зависимости проще, но это дает лишь частичное понимание ситуации.
Актуальность данных. Каждый сервис хранит свои данные, и их синхронизация между системами становится сложной задачей. Синтетические данные часто далеки от реальных, что снижает точность тестов. Чем больше сервисов, тем значительнее эта проблема. Сто́ит отметить, что это решаемая задача, требующая развития подхода Test Data as a Service, но такой подход предполагает значительные усилия и бюджет для обеспечения качества. Его реализация не всегда возможна из-за недостаточной готовности инвестировать в подобные преобразования.
Нет ясности, когда запускать тесты. Как определить, что система готова к проверке? Когда мы проводим тесты в контролируемой среде — как убедиться, что все будет работать так же хорошо в продакшене, где каждый сервис может иметь свои зависимости, а изменения происходят постоянно? Как обеспечить уверенность в том, что ваш код не нарушит работу других систем в условиях постоянных релизов?
Решение, которое предлагается в этой статье, — не серебряная пуля и не магия, которая решит все проблемы и даст вам 100% уверенности в завтрашнем дне. Но это одно из звеньев в процессе поставки продукта и повышения гарантий качества.
Расширяя канарейку
Мы в Купере используем стратегию канареечного релиза для быстрого получения обратной связи о новых версиях и одновременного снижения рисков за счет постепенного развертывания среди выбранных пользователей.
Один из применяемых нами методов — 1% canary. Новые функции раскатываются на 1% пользователей для сбора отзывов и оценки влияния на производительность и стабильность системы. Канареечные релизы снижают риски непредвиденных ситуаций, выявляя и устраняя проблемы на ранних стадиях. Больше подробностей — в статье «Как мы делаем канареечный деплой в PaaS».
Но что, если мы можем не просто наблюдать за тем, как канареечный релиз ведет себя на реальных пользователях, но и дать возможность специалистам по тестированию «пощупать» его? Какие преимущества это может открыть перед нами? И как такое решение способно помочь нам выявить потенциальные проблемы и уязвимости еще до того, как они станут очевидны в продакшене?
Эти вопросы стали для нас отправной точкой, когда мы задумались, как усовершенствовать процесс канареечного релиза. Мы пришли к идее расширить его возможности, добавив этап, который позволил бы нам еще тщательнее подходить к проверке качества продукта. Наша главная цель — не просто успешный релиз, а уверенность в том, что мы предлагаем пользователю продукт, соответствующий его ожиданиям и требованиям.
Особенно важно избежать ошибок в период высокого спроса. Для этого бизнесу и IT-подразделению необходимо быть уверенными в том, что процесс поставки не будет сопряжен с чрезмерным риском и может осуществляться в обычном режиме.
Итак, что у нас было на тот момент в арсенале? PaaS — мощный инструмент, который позволяет следующее:
выкатывать канареечные релизы;
полноценно управлять этим процессом, контролируя каждый этап — от подготовки релиза до его анализа;
гибко настраивать и конфигурировать релизы, что позволяет адаптировать процесс под конкретные потребности и сценарии.
Привычные возможности стали базисом, на котором мы решили построить нечто большее. Мы добавили в этот процесс еще один элемент — эксперимент.
Э-э-э-эксперименты
Как это работает?
Создание изолированной среды: превращаем этот pod в полноценную среду для тестирования, закрывая его от внешнего мира, чтобы избежать вмешательства реальных пользователей.
Доступ через маршрутизацию по заголовку: чтобы отдел тестирования мог взаимодействовать с приложением, доступ к pod предоставляется через маршрутизацию по заголовку. Таким образом, pod остается изолированным, но специалисты по тестированию могут пройти к нему, используя специальный заголовок.
Актуальная версия приложения: в pod загружается именно та версия приложения, которая будет развернута в продакшене. Это гарантирует, что мы работаем с актуальной версией продукта.
Интеграции с другими сервисами: учитывая, что это по факту реальное приложение, оно имеет все интеграции, которые доступны для сервиса. Это позволяет получить полноценную картину того, как приложение будет работать в реальной среде (потому что практически мы ее и построили).
Если вы скорее визуал, можно поразглядывать картиночку:
Если вы скорее визуал, можно поразглядывать картиночку:
В результате тестирование получает изолированную, но актуальную и полнофункциональную среду для тестирования, где можно проверять работу приложения.
Как обычно, без «но» не обошлось: этот эксперимент блокирует релиз, тем самым ограничивая набор тестов, которые можно прогнать. Чтобы не тормозить процесс, мы решили, что время на тестирование не должно превышать трех минут. Учитывая это ограничение, мы начали думать, что можно успеть проверить за такой короткий срок.
И тут мы вспомнили про три заветные буквы, которые менеджеры часто царапают на заборе.
CJM — Customer Journey Mapping, или карта путешествия клиента (не пользователи, а Даша-путешественница какая-то). Наверняка все тут в курсе, что это за зверь, но если в двух словах, то это своего рода критический путь пользователя, по которому он проходит для того, чтобы остаться довольным сервисом. В идеале этот путь охватывает не весь продукт, а только ключевой фрагмент его функционала.
Если взглянуть на путь нашего пользователя верхнеуровнево, вырисовывается такая схема:
Авторизация → Выбор адреса доставки или самовывоза → Выбор магазина/ресторана → Поиск товаров и добавление их в корзину → Оформление заказа.
Задача была ясна: протестировать этот фрагмент, чтобы убедиться, что все работает как должно. Дело за малым — написать тесты и внедрить их в процесс без лишней сложности.
Не будем углубляться в детали выбора языка для тестов. Честно говоря, придерживаюсь мнения, что особой разницы нет, главное, чтобы тесты приносили пользу. Однако сто́ит отметить, что в рамках эксперимента удобнее хранить тесты не в отдельном репозитории, а прямо рядом с бизнес-логикой на ее языке. Это помогает сохранить согласованность с кодом, давая следующие преимущества:
Разработка понимает, что делает тестирование, , а тестирование может обращаться за помощью к разработчикам.
Очевидный набор сложностей также присутствует и напрямую связан с рынком специалистов. Однако это уже совсем другая история…
Как все это запустить на практике? Мы решили не изобретать велосипед и подошли к тестам как к полноценному сервису. Для этого мы упаковали их в контейнер, который можно развернуть рядом с продакшен-контейнером в рамках эксперимента. Такой подход обеспечивает все необходимые доступы и функциональные возможности благодаря использованию общего namespace, что делает процесс максимально удобным и эффективным.
В итоге мы получили:
Тестовую среду с кодом релизной версии — окружение, где содержится код, готовящийся к выпуску.
Тесты, проверяющие критический путь (CJM). Теперь можно фокусироваться на главных аспектах пользовательского опыта.
Скомбинировав автоматизацию тестирования критического пути, изолированную тестовую среду и гибкость настройки релиза, мы достигли максимальной прозрачности и управляемости.
Исходя из этого, у нас получилось формализовать следующее флоу для релизного цикла с экспериментом:
Релиз-инженер запускает релиз (а бетономешалка мешает бетон).
Собирается будущая версия приложения.
Собирается приложение с нашими тестами (все происходит в рамках одной релизной ветки/тега).
Поднимается версия приложения в рамках эксперимента (туда никто не может попасть, кроме наших тестов или тех, кто знает как туда попасть).
Контейнер с тестами поднимется рядом с экспериментом.
Тесты запускаются и через заголовок попадают в наш эксперимент.
Происходит запуск CJM-тестов.
Отправляются отчеты, метрики и уведомления.
Дальше мы попадаем в развилку:
Если тесты прошли успешно — автоматически запускается деплой новой версии приложения через обычную канарейку. Контейнер с экспериментом и тестами схлопывается.
Если что-то пошло не по плану — деплой автоматически отменяется, новая версия приложения не раскатывается для пользователей.
Разбор ошибок достаточно прост.
Ошибки в эксперименте:
Призовите дежурного согласно эскалации, указанной в отчете.
Призовите дежурного по тестам.
Перезапустите деплой без эксперимента.
Ошибки в эксперименте возникают редко, однако к ним надо быть готовыми.
Ошибки в коде продукта:
Если получилось найти проблемный МR:
Если ошибку можно быстро пофиксить, то ждем хотфикс от разработчика и катим его отдельным релизом. Если фикс не нужно тестировать, то перед мерджем в мастер можно не ждать прохождения всего пайплайна, достаточно только зеленых тестов.
Если исправить ошибку быстро не получится, делаем реверт и заливаем его вместе со всеми предыдущими изменениями новым релизом.
Если проблема не решается быстро и известен проблемный MR — закатываем реверт этого MR отдельным релизом.
Для тех, кому проще воспринимать информацию через визуал, предоставляю такую возможность:
К достоинствам можно отнести:
Фокус на ключевом флоу (CJM): проверяются только критически важные пользовательские сценарии, что упрощает поддержку тестов, так как меньше факторов отказа.
Согласованность кода и тестов: тесты, расположенные рядом с бизнес-логикой, остаются актуальными. Это улучшает взаимодействие между командами разработки и тестирования, позволяя быстрее выявлять и исправлять ошибки. Тесты проходят через общий флоу с разработкой, актуализация тестов идет синхронно с разработкой в одной ветке.
Гибкость среды: тестовые контейнеры изолированы и легко разворачиваются рядом с экспериментальной версией приложения, обеспечивая доступ ко всем необходимым ресурсам в безопасных условиях.
Минимальный риск для пользователей: новая версия приложения доступна исключительно для тестирования, что предотвращает влияние возможных ошибок на конечных пользователей.
Низкие затраты на поддержание среды: контейнеры с тестами и экспериментальной версией приложения имеют короткий жизненный цикл (обычно не превышает трех минут), что снижает потребление ресурсов и экономит затраты на инфраструктуру.
Полная пирамида тестирования: этот подход, по сути, создает полную пирамиду обеспечения качества, так как позволяет автоматически валидировать поставку во время интеграции. Это гарантирует, что код проверяется на ключевых сценариях и соответствует требованиям, минимизируя риски и повышая уверенность в его стабильности на всех этапах разработки.
Из недостатков наиболее выделяется:
Сложность настройки среды: первые шаги были трудными. Однако мы верим, что эти затраты времени и ресурсов оправдаются в долгосрочной перспективе. На запуск и отладку в нескольких проектах ушел квартал. После обкатки и написания инструкций процесс пошел значительно быстрее. Обычно на новом проекте полный цикл, состоящий из настройки CI/CD и старта из минимального набора тестов, не превышает одного-двух дней.
Дополнительная нагрузка на релиз-инженера: хотя процесс не вызвал чрезмерных сложностей, он потребовал от релиз-инженера дополнительных действий, что временно увеличило его загрузку.
Каковы выводы по эксперименту?
Описанный подход позволяет прогонять не только функциональные и визуальные тесты, но и тесты производительности (например, через Lighthouse) или любые проверки, которые вы сможете придумать. Эксперимент предоставляет реальное боевое окружение со всем зоопарком сервисов и интеграций непосредственно перед релизом новой версии.
Несмотря на то, что эти эксперименты все еще являются экспериментами у нас в компании, они уже позволили предотвратить несколько инцидентов на этапе релиза. Это подтверждает нашу гипотезу, что даже частичное внедрение такой практики способно повысить стабильность и предсказуемость поставок, минимизируя риски для конечных пользователей.
CJM — это палка о двух концах. С одной стороны, он обеспечивает надежность критического пути при релизе, но с другой — оставляет без внимания остальные части системы. Поэтому мы не собираемся отказываться от традиционного подхода к тестированию. В нашем процессе по-прежнему остаются важные этапы, такие как полноценные E2E-тесты и тщательное ручное тестирование.