Когда мы в NTechLab первый раз задумались о проведении нагрузочных тестов для наших продуктов, выбор инструмента для нас был очевиден: JMeter имел мощное комьюнити, обширный набор доступных плагинов и возможность написать свой при необходимости. Немаловажным фактором стало и то, что в интернете масса статьей о том, как начать работать с JMeter.
Однако, очень скоро мы столкнулись с ситуацией, когда количество кейсов нагрузочного тестирования разрослось, а написанные нами скрипты в JMeter стали напоминать нагромождение бессвязных элементов. С каждым релизом поддерживать тестовый набор таких скриптов становилось всё сложнее. Сюрпризом стало и то, что несмотря на большое количество статей посвященных старту работы с JMeter, статей которые бы рассказывали о том как грамотно развивать нагрузочные тесты в JMeter – не нашлось.
Мы в NTechLab проводим много нагрузочных тестов для своих продуктов и в этой статье расскажем какими трюками пользуемся, чтобы наши JMeter тесты всегда были легко поддерживаемыми и читаемыми, а регрессионное нагрузочное тестирование продуктов не становилось пыткой.
Итак, как выглядит типичный скрипт нагрузочного тестирования в JMeter? Это набор конфиг-элементов, сэмплеров, листнеров, пре- и пост-процессоров которые определяют функциональный сценарий нагрузки. Все эти элементы объеденены под тред-группой, настройками которой мы определяем интенсивность данного сценария нагрузки. Если профиль нагрузки включает в себя несколько функциональных сценариев – значит в скрипте будет несколько тред-групп, каждая из которых будет реализовывать свой функциональный сценарий с заданной интенсивностью.
DISCLAIMER: если параграф выше ввёл вас в легкий ступор, то рекомендуем прочесть про базовые принципы построения JMeter скриптов и сущности, из которых они состоят, например эту статью: https://www.tutorialspoint.com/jmeter/jmeter_quick_guide.htm
Рассмотрим пример одного из нагрузочных сценариев для нашего продукта FindFace Multi: системы видеоаналитики с функциональностью распознавания людей и автомобилей.
В классическом нагрузочном тестировании мы имитируем действия пользователей или сторонней системы, которая генерирует нагрузку на нашу тестовую систему. Предположим, что нам дали список самых популярных и нагруженных операций, которые нужно протестировать и найти максимум системы или проверить стабильность работы под определенной нагрузкой.
Если подходить к задаче «в лоб», то итоговый скрипт в JMeter будет представлять примерно такую «портянку»:
Реализовать нагрузку подобным образом и одноразово протестировать продукт мы можем. Но как только речь идет о регрессионном нагрузочном тестировании каждый релиз, то сталкиваемся со следующими проблемами:
-
Поменялся функционал продукта и нужно добавить новую операцию в нагрузочный тест. Допустим мы хотим убрать операцию по переходу во вкладку “Камеры”, а вместо нее добавить переход во вкладку “Настройки”.
Мы сделали «disable» блока по переходу на вкладку “Камеры” (Simple Controller - Вкладка "Камеры", не удалять же рабочий кусок кода!) и добавили операцию по переходу в “Настройки” (Simple Controller - Вкладка "Настройки"). Так можно продолжать и дальше, но довольно быстро скрипт превратится в тяжело читаемое нагромождение элементов. Надо как-то изолировать полезные, но пока ненужные части нагрузочных скриптов.
Бывает так, что в разных частях тест плана используется один и тот же функциональный модуль, например “Выход из профиля”. Появляется необходимость копировать одно и то же действие. Очевидным, но худшим решением в данном случае будет копи-паста: при изменении API нам придется изменять повторяющиеся элементы во всех копиях. Если бы это были классические функциональные coded-тесты, то можно было бы вынести общую функциональность с отдельный класс или даже модуль, но как с этим быть в рамках JMeter скрипта?
По итогу мы вывели определенные подходы при написании скриптов нагрузочного тестирования, о которых сегодня хотели бы рассказать и показать, что не такой уж и “злой” GUI у Apache JMeter. Элементы данных подходов можно встретить обрывочно в разных мануалах в JMeter на просторах интернета, но цель нашей статьи – собрать воедино лучшие практики по систематизации JMeter скриптов и показать как этим пользуемся мы в условиях регулярных нагрузочных тестов живого, развивающегося продукта.
Иерархия и уровень вложенности элементов
В JMeter необходимо обращать внимание на иерархию и уровень вложенности элементов в нагрузочном скрипте.
Конфигурационные элементы (Config Element), о которых мы поговорим, ниже могут быть размещены на первом уровне иерархии и таким образом применяться для всех sampler-ов, которые ниже по уровню иерархии.
Сами Sampler-ы и Logic Controller-ы нельзя расположить в нашем Test Plan-e. Чтобы их использовать необходимо добавить Test Fragment или Thread Group.
Соответственно, остальные элементы, такие как Timer, Assertions, Pre-processors, Post-processors, Listeners будут применяться ко всем дочерним элементам с точки зрения уровня вложенности. Рассмотрим пару примеров.
-
Если мы хотим применить, например, Timer к одному Sampler-y, то таймер нужно вложить внутрь данного Sampler-а.
-
Если мы хотим применить Timer к нескольким Sampler-ам то он должен находиться на одном уровне вложенности с данными Sampler-ами.
Аналогично работает и с остальными элементами. Таким образом мы вынесли Config Element-ы на самый 1-й уровень вложенности и переменные из User Defined Variables 1, настройки из менеджеров будут глобально определены для каждого потока и его вложенных Sampler-ов.
Config Elements с номером 1,2,3,4 будут применены к Thread Group 5, Thread Group 6;
JSR223 PreProcessor 4 будет применен ко всем Sampler-ам в Thread Group 5, thread Group 6;
JSR223 PreProcessor 5.1, User Defined Variables 5.2 будут применены к элементам в Thread Group 5. В случае с User Defined Variables 5.2 и совпадающих переменных из User Defined Variables 1, применятся будет последнее полученное значение с точки зрения иерархии, то есть итоговое значение у переменной в Thread Group 5 будет из User Defined Variables 5.2.
HTTP Header Manager 5.3.1, 6.2.1 применится лишь к тому Sampler-у в который он вложен, а вот HTTP Header Manager 3 применится ко всем Sampler-ам, которые ниже по уровню иерархии.
View Result Tree 7 применится ко всем Thread Group и таким образом мы увидим результаты выполнения Sampler-ов из Thread Group 5, Thread Group 6. В случае с View Result Tree 6.4 будут зафиксированы результаты выполнения только Thread Group 6.
В нашем случаем тестирование осуществляется по протоколу HTTP, поэтому глобально необходимо определить следующие конфигурационные элементы:
User Defined Variables: С помощью данного Config Element-а мы можем определить константные локальные переменные для каждого потока. Давайте сделаем это верхнеуровнево, так как изменить нашу переменную можно на необходимом уровне выполнения “внутри” нашего нагрузочного сценария.
bzm - Random CSV Data Set Config : Также очень полезный Config Element без которого не обойтись. Представляет собой обработчик csv файла, в котором мы держим наш датасет. Определим его верхнеуровнево с соотвествующими настройками для его использования несколькими Thread-Group-ами, вместо того, чтобы добавлять его в каждую группу потоков.
HTTP Cache Manager, HTTP Cookie Manager, HTTP Header Manager, HTTP Request Defaults: конфигурационные элементы по протоколу http, где мы также можем определить глобально http header-ы, настройки Cache и Cookie Manager для всех дочерних элементов, которые ниже по уровню вложенности.
Получилось следующее:
И это наш первый совет. Определять глобально переменные, которые не меняются и применяются для всех Sampler-ов. Ведь ничего не мешает переопределить их внутри конкретной группы потоков, а то и в самом Sampler-e, если это будет нужно для отладки, например.
Переиспользование тестовых элементов
Далее расскажем про Logic Controller-ы, которые используются в каждом нашем скрипте:
Simple Controller - не предлагает никакой функциональности для выполнения вашего теста, кроме предоставления контейнера для хранения семплеров, пост и пре обработчиков.
Transaction Controller - данный контроллер имеет возможности Simple Controller-a, а также выводится в отчетах jmeter-а, суммируя время выполнения вложенных в него Sampler-ов. Применять данный контроллер рекомендуется, когда у нас повторяется http запрос в разных тест-кейсах и в случае ошибки мы хотим видеть в каком именно сценарии произошла ошибка. Также отмечу, что можно выводить время, суммируя помимо запросов время pre и post процессоров.
Module Controller - позволяет ссылаться на контроллеры, содержащие дочерние элементы. Таким образом во время выполнения теста на этапе нашего Module Controller-а будет выполняться тот элемент, на который он ссылается.
Вернемся к нашему скрипту и применим Logic Controller-ы на практике:
Введем следующий элемент тест-плана в JMeter: Test Fragment - это особый тип контроллера, который существует в дереве плана тестирования на том же уровне, что и элемент «Thread group». Он отличается от группы потоков тем, что не выполняется, если на него не ссылается Module Controller.
Мы добавляем Test Fragment с названием TF::BLOCKS, где будут расположены наши части тест кейсов и TF::CASES, где будут собраны наши нагрузочные сценарий по тест-кейсам, которые были представлены выше. Также добавим “thread group” под названием DEBUG, откуда будем ссылаться на описанные элементы для их запуска и отладки.
Скрипт мы реализуем по тест-кейсам, которые упоминались выше. Взглянем на них еще раз:
В TF::BLOCKS мы реализуем наши части тест кейсов, логически разделив их на действия. В нашем случае необходимо реализовать:
Авторизацию (встречается в тест кейсе № 1 и № 4)
Переход на главную страницу (встречается в тест кейсе № 1 и № 4)
Открытие вкладки “камеры” (встречается в тест кейсе № 1)
Открытие вкладки “события” (встречается в тест кейсе № 4)
Выход из профиля (встречается в тест кейсе № 1 и № 4)
Отправка детекта по лицу (встречается в тест кейсе № 2)
Отправка детекта по авто (встречается в тест кейсе № 3)
Использовать будем Transaction Controller-ы., так как нам важно понять, какая часть тест-кейса фэйлится, в случае ошибки.
Посмотрим на промежуточный итог:
Мы выделили из тест-кейсов логические блоки, далее реализуем их в Test Fragment-е под названием TF::BLOCKS. Для отладки сценариев у нас есть thread group под названием TG::DEBUG, где с помощью Module Controller-ов конструируем наш сценарий. Выглядит это так:
Продолжаем работать придерживаясь концепции, описанной выше, реализовывая части тест кейсов в виде логических блоков, периодически отлаживая наш сценарий и по итогу получился следующий скрипт:
Одним из заключительных шагов, собираем наши тест-кейсы из «блоков», которые мы реализовали ранее:
Вывод
Ну вот и вся магия. Только благодаря этим нехитрым практикам нам удалось существенно сократить дублирование кода в нагрузочных скриптах и сократить издержки при проведении регулярных регрессионных нагрузочных тестов.
Ещё раз, тезисно, опишем плюсы, которые мы получили от применения описанных выше подходов в составлении JMeter скриптов:
Параметризация за счет глобальных переменных: датасет можно менять удобно и быстро сразу для всех нагрузочных сценариев.
Гибкое конструирование нагрузочного сценария. Пропадает необходимость “нагромождать” тестовый набор лишним и повторным копированием. Достаточно просто добавить Module Controller со ссылкой на необходимый блок – и собирать новые сценарии как конструктор Lego.
Упрощенная поддержка при изменениях в сценарии. Например у нас поменялся порядок выполнения запросов в сценарии или обновилась API-шка в новой версии продукта. В этом случае достаточно изменить логику в внутри TF::BLOCKS и за счет ссылок из Module Controller-ов новая логика автоматически подтянется во все тестовые сценарии, где она используется.
И конечно же, эти подходы универсальны и не зависят от протокола который вы тестируете. В примерах мы приводим HTTP API, но те же правила мы применяем и при нагрузке на брокеров очередей сообщений (Message Queue Broker), и тестах баз данных (Database).
Если появились вопросы – будем рады ответить в комментариях!
Над статьей работали Никита Токарев и Андрей Глазков, группа нагрузочного тестирования, NTechLab.