Всем привет! В команде Мир Plat.Form я занимаюсь системами сертификации эмиссии – наш отдел разрабатывает программные продукты для внутренних и внешних пользователей, автоматизирующие сертификацию всего, что в конечном итоге превращается в «кредитку» в вашем кармане, начиная с карточных модулей и заканчивая дизайном карты. До недавнего времени моей основной задачей была автоматизация тестирования для нашей команды, и вот в один прекрасный день, около года тому назад, нам «внезапно» понадобилось протестировать производительность некоторых наших веб-приложений. Задача достаточно тривиальная, поэтому, без долгих раздумий, для её решения был выбран Apache JMeter – пусть он и не идеален, но если нужно что-то нагрузить без затяжной подготовки – самое то. Ничего не предвещало беды, были подготовлены стенды, подключен мониторинг, написаны сценарии и выполнено само тестирование, написан и разослан всем заинтересованным лицам отчёт о результатах. Мы даже нашли и поправили пару узких мест, что позволило значительно снизить нагрузку на БД и повысить производительность системы. Думаю, читатель уже догадался, что, если бы всё было так просто – этой статьи бы не было, но тут вдруг появилась потребность развивать продукт, регулярно выкатывая новые фичи, и мы поняли, что разовым тестированием мы уже не обойдёмся. Пришлось придумывать, как вписать регулярное проведение тестирования производительности в наши процессы CI/CD, используя имеющиеся инструменты. В нашей команде для автоматизации тестирования используется Java и связка TestNG / Cucumber / Allure, для задач CI/CD мы пользуемся Jenkins, так что и наше «непрерывное нагрузочное тестирование» было решено делать, не отступая от стандартного стека – таким образом мы не только не плодим зоопарк технологий, но и упрощаем подключение новых автоматизаторов. Раз мы здесь занимаемся в основном функциональным тестированием – почему бы не превратить нагрузочные тесты в функциональные? Пусть сценарии пишутся на Gherkin, запускаются через TestNG + Cucumber внутри Jenkins, и туда же выкладываются сформированные Allure отчёты! Сам Apache JMeter позволяет запускать сценарии и обрабатывать результаты их выполнения из Java-кода, единственное, что потребуется дополнительно – это дистрибутив Jmeter или запущенный сервер-агент JMeter (мы использовали оба подхода, но в данной статье речь пойдёт только об использовании дистрибутива, перейти на использование агента при необходимости будет очень просто). Для агрегации результатов мы воспользовались библиотекой Math3 от того же Apache, она позволяет собирать нужные нам значения и затем выдавать рассчитанные показатели, такие как среднее значение, медиана, перцентили и так далее. В итоге получился примерно такой набор зависимостей: Из-за давней «особенности» библиотек JMeter приходится в Gradle исключать модуль bom, иначе проект не будет собираться. Так как тесты будут запускаться с использованием TestNG и Cucumber, а отчёты будут формироваться в Allure – добавляем плагин и настраиваем таски: Архив с дистрибутивом JMeter и файл jmeter.properties (он понадобится для инициализации) размещаем в main/resources. Стандартный runner для Cucumber-тестов, ничего нового: Для удобства подготовки движка JMeter, загрузки и выполнения сценариев, создаём класс JMeterTestExecutor. В нём будем хранить нужные для работы пути и, собственно, сам движок: Конструктор принимает параметры с версией JMeter (заложено, скорее, «на вырост», на текущий момент у нас нет причины использовать несколько различных версий) и именем properties-файла, распаковывает лежащий в ресурсах дистрибутив во временную директорию, кладёт туда же properties и инициализирует движок: Код распаковки архива и копирования файлов во временную директорию особого интереса не представляет, также, как и загрузка сценария, а вот непосредственное выполнение сценария уже содержит некоторые интересные моменты: Здесь используется кастомный listener, который как раз и будет агрегировать результаты выполнения теста для их дальнейшего анализа. Для того, чтобы в процессе выполнения сценария JMeter сохранять результаты, мы реализовали простой listener, главная функция которого – передача результата в класс сбора статистики. Чтобы не использовать сложные структуры данных, для хранения общих результатов, результатов успешно выполненных запросов и статистики в разрезе лейблов и кодов ответа используются отдельные объекты агрегатора, по которым раскладываются значения в зависимости от полученных результатов выполнения: Класс JMeterStatisticsAggregator служит, по сути, обёрткой над хранилищем собранных значений, в качестве которого используется класс DescriptiveStatistics из пакета Apache Math3. Здесь же реализованы методы для получения рассчитанной статистики: Пожалуй, самым сложным в итоге оказалось обернуть выполнение тестов в привычный нашей команде Gherkin. Сама по себе концепция нагрузочного тестирования довольно слабо пересекается с тестированием функциональным, здесь нет как таковых шагов – по сути, мы только проверяем результаты и сравниваем их с эталонными (поэтому практически сразу было решено саму загрузку и прогон нагрузочного теста вынести в @BeforeAll, а шаги делать только для проверок, чтобы результаты красиво легли в Allure). В итоге feature-файлы превратились в наборы таблиц с перечислением параметров и их эталонных значений, примерно такие: Так как различных параметров достаточно много и, кроме того, их значения требуется сравнивать с эталонными в зависимости от условий, реализация шагов была сделана максимально обобщённой: Основная логика получения значений параметров и их сравнения была спрятана в хитрые enum’ы, внутри которых используются лямбды, берущие на себя всю грязную работу. Для выполнения сравнения значений достаточно вызвать функцию compare() соответствующего enum’а, реализация же самого сравнения отличается в зависимости от операции: Аналогичным образом реализовано и получение значений параметров из статистики выполнения теста – достаточно вызвать функцию evaluate() и передать в неё объект с агрегированной статистикой, за остальное отвечают лямбды: Таким образом, в сценариях Gherkin мы можем использовать удобные для восприятия названия параметров и операций сравнения, а вся дополнительная логика предоставляется соответствующими классами и является максимально универсальной. В итоге мы получили небольшой фреймворк, который позволяет загрузить в него практически любой сценарий нагрузочного тестирования, выполнить тесты и проанализировать результаты. К сожалению, используемые средства не позволяют посмотреть, например, статистику по изменению времени ответа на запрос – мы проверяем только то, соответствует ли время ответа ожидаемому. Однако при использовании Jenkins и Allure мы видим график успешных и неуспешных тестов, и можем вовремя заметить, что в новой сборке приложения какие-то из запросов стали работать медленнее допустимого или выдавать ошибки. В статью не вошла реализация проверок для отдельных запросов по их именам (лейблам) и проверка кодов ответа сервера, этот функционал реализован аналогично проверкам общего результата. Для текущих задач имеющихся возможностей нам оказалось достаточно, но, пожалуй, приятно было бы видеть не просто графики прохождения тестов, а статистику конкретных значений времени ответа. Сейчас мы рассматриваем два варианта реализации этого функционала – сохранение данных в InfluxDB и использование Grafana для построения графиков, или реализация собственного решения, некоего портала с результатами нагрузочных тестов. Какой бы вариант решения мы ни выбрали, внедрение дополнительных возможностей в имеющийся фреймворк будет заключаться только в добавлении новой точки агрегации результатов в класс JMeterTestStatistics, что не составит особого труда. Регулярный прогон нагрузочных тестов позволяет нам быть уверенными в том, что изменения наших систем не приводят к деградации производительности и на раннем этапе отслеживать и исправлять возникающие проблемы.