Тестирование микросервисной архитектуры (MSA/Microservices)

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

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

Само тестирование можно разделить на следующие виды:

  • unit тестирование;

  • контрактное тестирование;

  • интеграционное тестирование;

  • end-to-end тестирование;

  • нагрузочное тестирование;

  • UI или функциональное тестирование.

Unit тестирование

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

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

Unit тестами у нас покрыто 70% функционала, и так как мы применяем CI/CD, пока они не пройдены, приложение не задеплоится.

Контрактное тестирование

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

Для этого нужен контракт между командами (в нашем случае мы используем Pact), который будет содержать все методы и возвраты для всех сервисов.

Например, бэкэнд-разработчик написал код, проставил аннотации и сделал swagger-документацию. Но если swagger не провалидируется фронтендом, а QA его уже протестируют ­- мы просто зря потратим время. Поэтому и создается контракт: например, у сервиса 8 эндпоинтов, и мы знаем, в каком формате он отдаёт и принимает данные.

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

У нас оно работает следующим образом: с самой ранней стадии есть техническое задание, согласованное со всеми стейкхолдерами. На основе ТЗ проходит оценка задачи и создается схема, согласно которой все будут работать.

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

Интеграционное тестирование

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

Так как процесс тестирования был внедрен на ранних этапах разработки, каждый отдельный сервис пришлось поднимать локально, а все зависимости других модулей - мокать. В качестве языка была выбрана Java (с момента основания компании этот язык используется для написания тестов).

Что касается сборщика, то все тоже максимально просто: наша текущая архитектура изначально использует Gradle.

Если рассмотреть нашу инфраструктуру автоматизации, то это по большей части кастомный проект, в основе которого Java и Gradle, плюс куча библиотек, таких как Junit5, Feign, Rest Assured и т.д.

Feign и Rest Assured используется вместе потому, что до перехода на микросервисную архитектуру наш проект прекрасно жил на Feign. Как библиотека для покрытия API это было лучшее решение. Но после перехода на новую архитектуру по факту вся платформа была переписана на микросервисы, которые нужно покрыть верхнеуровневыми тестами в короткий срок для возможности дальнейшего проведения интеграционного тестирования. Тут мы и подключили вторую библиотеку Rest Assured, что позволило быстро покрывать огромные куски функционала (для многих данное решение будет спорным, но на тот момент большинство новых QA работало именно с Rest Assured, что и стало решающим фактором при выборе новой библиотеки).

Для развертывания окружения в docker-контейнерах локально или на CI-сервере используется Java-библиотека TestContainers, которая позволяет оркестрировать docker-контейнерами непосредственно из кода тестов (testcontainers.org). При развертывании окружения поднимаются сам тестируемый сервис, а также используемые сервисом базы данных, брокер сообщений и эмулятор, который и мокает все внешние сервисы.

Так как все контейнеры мы поднимаем локально и на ранних этапах разработки, то потребовалось очень много времени на то, чтобы настроить перманентное окружение. Например, у нас есть сервис Settings, которому для работы нужны Сервисы 1 и 2, и какие-то данные с Kafka и MINO. Все это берется из переменных окружения, и за счет огромного количества зависимостей тяжело контролировать процесс поднятия одного сервиса.

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

End-to-end тестирование

По сути своей E2E тестирует бизнес-логику, так же, как и в интеграционном, но уже не изолированно, а в масштабе всей системы.

В end-to-end тестировании мы проверяем взаимодействие всех сервисов c платформой:

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

Нагрузочное тестирование

Процесс нагрузочного тестирования будем формально разделять на 4 небольших этапа:

  • тестирование производительности (Performance Testing) - исследование времени отклика ПО при выполнении операций на разных нагрузках, в том числе на стрессовых нагрузках;

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

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

  • объемное тестирование (Volume Testing) - исследование производительности ПО для прогнозирования долгосрочного использования системы при увеличении объемов данных, то есть анализ готовности системы к долгосрочному использованию.

Для тестов мы используем JMeter, а сами нагрузочные скрипты написаны на Groovy.

Мы используем около пяти виртуальных машин, развернутых на AWS и у нас есть 7 физических машин. Последние используем, если нужно создать большую нагрузку - 15,000 RPS и больше. Виртуальные машины, так как они, по сути, являются «откусанными» частями одной большой машины, таких цифр показать не могут - каждый реквест нужно отправлять с подписью шифрования, и это сильно нагружает процессор. Так что VM используем для фоновой или статической нагрузки в районе 2000 RPS.

Статистику собираем в Grafana - анализируем все показатели, нагрузку на CPU, GPU, сеть, диски и т.д.

Сначала сравниваем с эталонными показателями, потом экспериментируем, например, нагружаем какое-то время процессор на 30%, делаем короткий скачок до 90%-100%, и смотрим, сколько битых запросов нам нападает.

UI или функциональное тестирование

Это завершающий этап тестирования. Если в предыдущих тестах фигурировал только API, теперь тестируется и фронт. Проводим как мануальное тестирование, так и автотесты.

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

Мы используем Java, Cucumber и самописную библиотеку для описания логики сценариев (раньше использовали Akitа сценарии, но поддержка библиотеки закончилась на Java 8, нам пришлось написать свою библиотеку для работы с UI-тестами, но в основе лежат методы именно оттуда). Cucumber используем для удобства написания самих тестов.

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

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

Например, нужно протестировать email-шаблоны, которых у нас 100-200. В один поток это займет 15-20 минут. Поэтому создаем pipeline в Jenkins, который этот скрипт разбивает на много маленьких контейнеров, которые поднимаются в Selenide.

Можно сказать, что Selenide - это виртуальный браузер, а Selenium - виртуальный пользователь. Одновременно поднимаются 10 контейнеров, и все тесты проходят за пару минут. Все пайплайны тоже написаны на Groovy.

После этого собираем это все в отчеты в зависимости от проекта: UI в Cucumber reports, а API-тесты - в Azure.

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

Источники:

Доп. материал:

Last updated