Поддерживаемые тесты в JMeter: tips and tricks

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

Однако, очень скоро мы столкнулись с ситуацией, когда количество кейсов нагрузочного тестирования разрослось, а написанные нами скрипты в JMeter стали напоминать нагромождение бессвязных элементов. С каждым релизом поддерживать тестовый набор таких скриптов становилось всё сложнее. Сюрпризом стало и то, что несмотря на большое количество статей посвященных старту работы с JMeter, статей которые бы рассказывали о том как грамотно развивать нагрузочные тесты в JMeter — не нашлось.

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

Итак, как выглядит типичный скрипт нагрузочного тестирования в JMeter?  Это набор конфиг-элементов, сэмплеров, листнеров, пре- и пост-процессоров которые определяют функциональный сценарий нагрузки. Все эти элементы объеденены под тред-группой, настройками которой мы определяем интенсивность данного сценария нагрузки. Если профиль нагрузки включает в себя несколько функциональных сценариев — значит в скрипте будет несколько тред-групп, каждая из которых будет реализовывать свой функциональный сценарий с заданной интенсивностью.

DISCLAIMER: если параграф выше ввёл вас в легкий ступор, то рекомендуем прочесть про базовые принципы построения JMeter скриптов и сущности, из которых они состоят, например эту статью: https://www.tutorialspoint.com/jmeter/jmeter_quick_guide.htm

Рассмотрим пример одного из нагрузочных сценариев для нашего продукта FindFace Multi: системы видеоаналитики с функциональностью распознавания людей и автомобилей.

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

2c33e9b1acb76261824a6a886eedfd21.png

Если подходить к задаче «в лоб», то итоговый скрипт в JMeter будет представлять примерно такую «портянку»:

b7bdb114e0991acd942dacf7bb4a49a9.png

Реализовать нагрузку подобным образом и одноразово протестировать продукт мы можем. Но как только речь идет о регрессионном нагрузочном тестировании каждый релиз, то сталкиваемся со следующими проблемами:

  1. Поменялся функционал продукта и нужно добавить новую операцию в нагрузочный тест. Допустим мы хотим убрать операцию по переходу во вкладку «Камеры», а вместо нее добавить переход во вкладку «Настройки».

    6b93fded4eb6d8c45ab826662d29e500.png

    Мы сделали «disable» блока по переходу на вкладку  «Камеры» (Simple Controller — Вкладка «Камеры», не удалять же рабочий кусок кода!) и добавили операцию по переходу в «Настройки» (Simple Controller — Вкладка «Настройки»). Так можно продолжать и дальше, но довольно быстро скрипт превратится в тяжело читаемое нагромождение элементов. Надо как-то изолировать полезные, но пока ненужные части нагрузочных скриптов.

  2. Бывает так, что в разных частях тест плана используется один и тот же функциональный модуль, например «Выход из профиля». Появляется необходимость копировать одно и то же действие. Очевидным, но худшим решением в данном случае будет копи-паста: при изменении API нам придется изменять повторяющиеся элементы во всех копиях. Если бы это были классические функциональные coded-тесты, то можно было бы вынести общую функциональность с отдельный класс или даже модуль, но как с этим быть в рамках JMeter скрипта?

По итогу мы вывели определенные подходы при написании скриптов нагрузочного тестирования, о которых сегодня хотели бы рассказать и показать, что не такой уж и «злой» GUI у Apache JMeter. Элементы данных подходов можно встретить обрывочно в разных мануалах в JMeter на просторах интернета, но цель нашей статьи — собрать воедино лучшие практики по систематизации JMeter скриптов и показать как этим пользуемся мы в условиях регулярных нагрузочных тестов живого, развивающегося продукта.

Иерархия и уровень вложенности элементов

В JMeter необходимо обращать внимание на иерархию и уровень вложенности элементов в нагрузочном скрипте.
Конфигурационные элементы (Config Element), о которых мы поговорим, ниже могут быть размещены на первом уровне иерархии и таким образом применяться для всех sampler-ов, которые ниже по уровню иерархии.

115d17dc05e4e00075aac819ee6fa96a.png

Сами Sampler-ы и Logic Controller-ы нельзя расположить в нашем Test Plan-e. Чтобы их использовать необходимо добавить Test Fragment или Thread Group.

Соответственно, остальные элементы, такие как Timer, Assertions, Pre-processors, Post-processors, Listeners будут применяться ко всем дочерним элементам с точки зрения уровня вложенности. Рассмотрим пару примеров.

  1. Если мы хотим применить, например, Timer к одному Sampler-y, то таймер нужно вложить внутрь данного Sampler-а.

    1d19411bf71951cc8e3d91842612f49c.png
  1. Если мы хотим применить Timer к нескольким Sampler-ам то он должен находиться на одном уровне вложенности с данными Sampler-ами.

    c7f59434d20d2f8b4c075030f160f7cb.png

Аналогично работает и с остальными элементами. Таким образом мы вынесли Config Element-ы на самый 1-й уровень вложенности и переменные из User Defined Variables 1, настройки из менеджеров будут глобально определены для каждого потока и его вложенных Sampler-ов.

56b34265fdb8dd951bfef24bb67a793c.png

  • 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, поэтому глобально необходимо определить следующие конфигурационные элементы:

  1. User Defined Variables: С помощью данного Config Element-а мы можем определить константные локальные переменные для каждого потока. Давайте сделаем это верхнеуровнево, так как изменить нашу переменную можно на необходимом уровне выполнения «внутри» нашего нагрузочного сценария.

  2. bzm — Random CSV Data Set Config: Также очень полезный Config Element без которого не обойтись. Представляет собой обработчик csv файла, в котором мы держим наш датасет. Определим его верхнеуровнево с соотвествующими настройками для его использования несколькими Thread-Group-ами, вместо того, чтобы добавлять его в каждую группу потоков.

  3. HTTP Cache Manager, HTTP Cookie Manager, HTTP Header Manager, HTTP Request Defaults: конфигурационные элементы по протоколу http, где мы также можем определить глобально http header-ы, настройки Cache и Cookie Manager для всех дочерних элементов, которые ниже по уровню вложенности.

Получилось следующее:

b433c6d0525daae749903020a7acd6dc.png

И это наш первый совет. Определять глобально переменные, которые не меняются и применяются для всех Sampler-ов. Ведь ничего не мешает переопределить их внутри конкретной группы потоков, а то и в самом Sampler-e, если это будет нужно для отладки, например.

Переиспользование тестовых элементов

Далее расскажем про Logic Controller-ы, которые используются в каждом нашем скрипте:

  1. Simple Controller — не предлагает никакой функциональности для выполнения вашего теста, кроме предоставления контейнера для хранения семплеров, пост и пре обработчиков.

  2. Transaction Controller — данный контроллер имеет возможности Simple Controller-a, а также выводится в отчетах jmeter-а, суммируя время выполнения вложенных в него Sampler-ов. Применять данный контроллер рекомендуется, когда у нас повторяется http запрос в разных тест-кейсах и в случае ошибки мы хотим видеть в каком именно сценарии произошла ошибка. Также отмечу, что можно выводить время, суммируя помимо запросов время pre и post процессоров.

  3. Module Controller —  позволяет ссылаться на контроллеры, содержащие дочерние элементы. Таким образом во время выполнения теста на этапе нашего Module Controller-а будет выполняться тот элемент, на который он ссылается.

Вернемся к нашему скрипту и применим Logic Controller-ы на практике:

Введем следующий элемент тест-плана в JMeter:  Test Fragment — это особый тип контроллера, который существует в дереве плана тестирования на том же уровне, что и элемент «Thread group». Он отличается от группы потоков тем, что не выполняется, если на него не ссылается Module Controller.

Мы добавляем Test Fragment с названием TF: BLOCKS, где будут расположены наши части тест кейсов и TF: CASES, где будут собраны наши нагрузочные сценарий по тест-кейсам, которые были представлены выше. Также добавим «thread group» под названием DEBUG, откуда будем ссылаться на описанные элементы для их запуска и отладки.

eddbebe7b5c1006e2d44a274de9debf4.png

Скрипт мы реализуем по тест-кейсам, которые упоминались выше. Взглянем на них еще раз:

f106b85301574bd4382573188a524b23.png

В TF: BLOCKS мы реализуем наши части тест кейсов, логически разделив их на действия. В нашем случае необходимо реализовать:

  1. Авторизацию (встречается в тест кейсе № 1 и № 4)

  2. Переход на главную страницу (встречается в тест кейсе № 1 и № 4)

  3. Открытие вкладки «камеры» (встречается в тест кейсе № 1)

  4. Открытие вкладки «события» (встречается в тест кейсе № 4)

  5. Выход из профиля (встречается в тест кейсе № 1 и № 4)

  6. Отправка детекта по лицу (встречается в тест кейсе № 2)

  7. Отправка детекта по авто (встречается в тест кейсе № 3)

Использовать будем Transaction Controller-ы., так как нам важно понять, какая часть тест-кейса фэйлится, в случае ошибки.

Посмотрим на промежуточный итог:

40188594859784789a385695be8855d2.png

Мы выделили из тест-кейсов логические блоки, далее реализуем их в Test Fragment-е под названием TF: BLOCKS. Для отладки сценариев у нас есть thread group под названием TG: DEBUG, где с помощью Module Controller-ов конструируем наш сценарий. Выглядит это так:

02c92acbcc94da339af863542448a274.png

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

6a6a495da24f7831229c6adac1c8fa26.png

Одним из заключительных шагов, собираем наши тест-кейсы из «блоков», которые мы реализовали ранее:

7aa9897806950ed72c574ecff2534d33.png

Вывод

Ну вот и вся магия. Только благодаря этим нехитрым практикам нам удалось существенно сократить дублирование кода в нагрузочных скриптах и сократить издержки при проведении регулярных регрессионных нагрузочных тестов.

Ещё раз, тезисно, опишем плюсы, которые мы получили от применения описанных выше подходов в составлении JMeter скриптов:

  1. Параметризация за счет глобальных переменных: датасет можно менять удобно и быстро сразу для всех нагрузочных сценариев.

  2. Гибкое конструирование нагрузочного сценария. Пропадает необходимость «нагромождать» тестовый набор лишним и повторным копированием. Достаточно просто добавить Module Controller со ссылкой на необходимый блок — и собирать новые сценарии как конструктор Lego.

  3. Упрощенная поддержка при изменениях в сценарии. Например у нас поменялся порядок выполнения запросов в сценарии или обновилась API-шка в новой версии продукта. В этом случае достаточно изменить логику в внутри TF: BLOCKS и за счет ссылок из Module Controller-ов новая логика автоматически подтянется во все тестовые сценарии, где она используется. 

И конечно же, эти подходы универсальны и не зависят от протокола который вы тестируете. В примерах мы приводим HTTP API, но те же правила мы применяем и при нагрузке на брокеров очередей сообщений (Message Queue Broker), и тестах баз данных (Database).

Если появились вопросы — будем рады ответить в комментариях!

Над статьей работали Никита Токарев и Андрей Глазков, группа нагрузочного тестирования, NTechLab.

© Habrahabr.ru