Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, мы Алексей Чичук, Анастасия Стрижеченко и Владислав Литвинов — тестировщики из банка Точка.
И сегодня мы расскажем, как и почему мы используем JMeter для функционального тестирования.
Предыстория
Конечно же, в Точке огромное количество сервисов, каждый из которых совершенствуется и «обрастает» новыми фичами. Всё это невозможно протестировать руками, а обучать тестировщиков автоматизации на каком-либо языке программирования — долго, дорого, а местами и вовсе неэффективно. Поэтому мы начали искать инструмент автоматизации, который:
позволит быстро автоматизировать тестирование;
будет несложен в освоении тестировщиками разных уровней.
Мы начали экспериментировать с JMeter, и он себя хорошо показал: множество плагинов и огромное поле возможностей позволяли решать любые задачи автоматизации. Low-code-решение сделало вход намного легче и приятнее, чем изучать языки программирования с нуля, поднимать окружение и инфраструктуру для тестов. Время шло, мы становились опытнее, и этот инструмент плавно перешёл для нас из категории «удобный» в «незаменимый».
Что сейчас
В Точке нет строгих правил по выбору инструментов для тестирования, поэтому каждая команда может сама решать, какой использовать в работе. Сейчас 15% всех тестировщиков Точки пишут автотесты на Jmeter, хотя на старте им пользовалась только одна команда. На данный момент с его помощью написано более 6 тыс. автотестов.
Также мы разработали внутренний курс по обучению автоматизации на Jmeter. Так тестировщик сможет быстро освоить этот инструмент и сократить ручное тестирование в своей команде.
Почему не другие инструменты?
Изначально у нас были определённые требования к инструменту, от которых мы и отталкивались. Если сравнить, что умеет каждый из них, то увидим следующую картину:
В Точке используется единая TMS, которая принимает отчёты в формате Allure. Из таблицы видно, что ни один из рассмотренных low-code инструментов не имеет необходимого нам формата отчёта (причём у каждого инструмента вообще своя реализация).
Полагаем, что здесь знатоки Postman и Newman нам возразят, отметив, что такая связка умеет работать с Allure. Это, конечно, так, но реализация «один запрос — один тест в отчёте» — это совсем не то, что применимо в реальной практике тестирования.
Помимо проблем с отчётами, у того же Postman имеются проблемы с походом в базы данных. Конечно, есть сервисы по типу PostgREST и ему подобные, которые предоставляют интерфейс общения с базой по REST, но нам такое не нравится: плодить ради такого действия дополнительные сервисы — перебор. К тому же, это дополнительная точка отказа, которую иметь не хочется.
Закрыв глаза на проблемы с отчётностью, которые есть у всех рассматриваемых инструментов, мы выявили для себя лидера — это JMeter. Самый большой плюс этого инструмента для нас кроется в пункте «Возможность подпилить напильником», поскольку именно благодаря этому в пункте «Нормальный отчёт по прогону» можно сменить красный кружок на зелёный. Что мы, собственно, и сделали.
По итогу с выбором JMeter мы ни капли не ошиблись.
JMeter умеет многое. С его помощью вы сможете:
Пользоваться преимуществами огромного коммьюнити по всему миру;
Использовать готовые плагины и иметь возможность написать свой;
Писать автотесты на API без написания кода;
Обращаться к API и проверять содержимое ответов;
Отправлять и скачивать файлы;
Парсить JSON, YAML и CSV файлы и использовать данные из них;
Подключаться к базам данных и сервису очередей RabbitMQ;
Настроить автоматический запуск тестов на JMeter в CI и отправку отчётов в Allure, чтобы их было удобно просматривать.
Как написать тест на JMeter
HTTP Request, headers, assertions
Для общего понимания, немного пройдёмся по двум определениям:
Sampler (семплер) — элемент, который отправляет HTTP-, JDBC- и другие запросы;
Listener (лисенер) — компонент, показывающий результаты семплеров. Запрос/Ответ, время, заголовки, куки и т. д.
Перейдём к практической части и напишем простой тест. В качестве тестируемого сервиса возьмём форк известного приложения на Spring Boot — Spring Petclinic. Его особенность в том, что это серверная версия приложения и предоставляет только REST API. К тому же имеется база данных, а пользовательского интерфейса нет. Это нам подходит, так как в Точке много бэкенд-микросервисов, не имеющих UI.
Фундаментальным элементом для тестирования API является HTTP Request. Он позволяет отправлять HTTP-запросы к вашему API-серверу и получать ответы в формате JSON или XML. Создадим запрос на получение списка всех хозяев домашних животных.
Выглядит достаточно просто, собственно, как и в любых других приложениях, позволяющих отправлять HTTP-запросы. Вам необходимо знать базовый URL, порт и путь. Заголовок Content-Type: application/json можем прописать, добавив элемент HTTP Header Manager. Важная особенность — от того, где расположен этот элемент, зависит область действия. Если находится в корне Thread Group, то применятся ко всем HTTP Request. Если внутри конкретного HTTP Request — действует только для него.
Из коробки JMeter отображает JSON в одну строку — это неудобно. Поэтому подключаем плагин jp@gc — JSON Format Post Processor. Предварительно добавив элемент View Results Tree в наш Test Plan и выполнив HTTP-запрос, мы увидим результат.
Какой же тест без проверки? Например, мы хотим проверить, что в полученном списке у хозяина по имени Peter есть змея по кличке George. Так как в ответ нам приходит JSON, то логично для проверки использовать JSON Assertion. Этот элемент использует JSON Path. С его помощью мы можем делать совершенно любые проверки в JSON. Получилось вот такое jsonpath-выражение: $[?(@.firstName == 'Peter')].pets[*].name
, которое вернёт имя нужного нам питомца.
JSONPath — это язык запросов для JSON, который позволяет обращаться к любому из элементов JSON. Хороший навык для написания тестов, позволяющий указывать пути до элементов для явных проверок.
Специально сделаем опечатку в ожидаемом значении, чтобы убедиться в правильности проверки. Результат запуска покажет несоответствие ожидаемого и фактического результата.
Отлично! Первый тест готов. Далее мы сделаем так, чтобы тест был атомарным и мог быть зелёным сколько угодно раз, вне зависимости от данных.
Базы данных, переменные
Может произойти такое, что Peter с его питомцем больше нет в нашей базе данных. Логично, что наш тест сразу начнёт падать. Чтобы сделать тест независимым от данных, нужно добавлять данные перед тестом и очищать их после теста. Таким образом, тест всегда будет проходить вне зависимости от изменений в базе данных. Здесь есть два пути:
Перед проверкой наличия хозяина с питомцем в базе, использовать его непосредственное добавление по API. Есть POST-запрос
/petclinic/api/owners
, который добавит нужного нам хозяина в базу данных. Таким же образом добавим питомца к только что созданному хозяину ( POST/petclinic/api/owners/{ownerId}/pets
). После самой проверки использовать DELETE-запросы для очистки данных.Вместо использования API — сразу добавить нужного хозяина и питомца в базу данных. И также их очистить после теста. Не всегда есть возможность сделать тестовые данные, используя API. Приходится использовать базу данных — и в этом нет ничего плохого.
Рассмотрим оба случая.
В первом варианте с использованием API: добавляем HTTP Request с добавлением хозяина перед нашей основной проверкой.
Также добавим проверки: код ответа 201, firstName в ответе совпадает с тем, что мы указали в запросе. Добавим Response Assertion, чтобы проверить код ответа. И уже известный нам JSON Assertion для проверки значения в возвращаемом JSON.
В ответ запрос возвращает id
созданной сущности. С помощью JSON Extractor запишем их в переменную. Несколько слов про переменные в JMeter: в них можно записывать значения и использовать в дальнейших запросах, тестах. Использовать переменную можно по такому паттерну: ${var_name}
.
Значение id
нам пригодится в дальнейшем при удалении хозяина.
Аналогичным образом добавляем питомца к тестовому хозяину.
Исправляем нашу проверку на наличие добавленного хозяина в списке и проверяем, что появился тестовый хозяин, у которого змею зовут George.
После этой проверки добавляем два шага по удалению данных: удаляем питомца и самого хозяина. У нас получился атомарный тест, который не зависит от наличия данных в базе или от другого теста, и может запускаться сколько угодно раз:
Добавили тестового хозяина;
Добавили питомца к тестовому хозяину;
Получили список всех хозяев. Проверили, что наш есть в списке;
Удалили питомца;
Удалили тестового хозяина.
Теперь рассмотрим второй вариант с использованием базы данных. Подключиться к базе данных из JMeter можно через элемент JDBC Connection Configuration. Настроим подключение для нашей базы — используем postgresql. Необходимо задать нужные значения для подключения. Из особенностей только то, что необходимо придумать название для создаваемого пула соединений — например, PETCLINIC.
Выполнить запрос в базу данных позволяет элемент JDBC Request. Добавим его и напишем insert на добавление хозяина и его питомца. Указываем наш пул соединений — PETCLINIC. Запрос в базу данных получился такой:
insert into owners (first_name, last_name, address, city, telephone)
values ('Testname', 'Testlastname', '1 Lenina St.', 'Ekaterinburg', '1234567890') returning id;
Обратите внимание, что при добавлении записи в базу, мы сразу же возвращаем значения id
(для дальнейшего использования). Поэтому нам нужно выбрать тип запроса (Query Type) — Callable Statement.
Важно отметить: так как мы возвращаем значение id
, его можно записать в переменную, которую потом сможем использовать в других шагах. Однако есть особенность, обращаться к ней нужно будет по паттерну ${VAR_NAME_1}
. Именно с постфиксом _1
, потому что из базы данных возвращаются строки и их может быть несколько. Соответственно, если вам нужна не первая вернувшаяся строка, а третья, то используем постфикс _3
.
Точно таким же образом делаем добавление питомца, а после самой проверки — удаление этих же данных из базы по имеющихся у нас id
. У нас получился аналогичный тест, только данные были добавлены другим путём — через вставку записей в базу.
Довольно простыми действиями мы смогли написать полноценный тест, к тому же используя разные подходы. Познали мощь переменных для цепочек запросов и научились ходить в базу данных из Jmeter.
Процессоры
Перед использованием ранее полученных данных иногда их нужно обработать. Процессор выполняет какое-то действие около семплера. В Jmeter есть два типа процессоров: Pre-processor — выполняет действие до работы семплера; Post-processor — после.
Процессоров достаточно много, все они разные и используются в функциональном тестировании не всегда. Про часть из них уже было написано выше — JSON Extractor после запроса может сохранить переменную по jsonpath, Format Post Processor форматирует Response JSON в читаемый вид после выполнения семплера. Взглянем какие ещё процессоры можно использовать.
Regular Expression Extractor — используется для извлечения части переменной для дальнейшего использования. Например, мы получили в ответе ссылку в формате https://host/showcard/host/showcard/{token}
и нам нужно вставить в следующий запрос токен из этой ссылки. Для извлечения значения токена и подстановки его в переменную token
мы используем Regular Expression Extractor со следующими заполненными полями:
Если вы не смогли что-то сделать стандартными инструментами Jmeter, то на помощь придёт JSR223 Post-processor. В данном процессоре можно выполнять код на разных языках (java/groovy/javascript и т.д.). Кажется чем-то сложным, но всегда есть возможность загуглить и взять ту или иную реализацию.
Контроллеры
Контроллеры применяются для того, чтобы делать ветвления и циклы. Расскажем про контроллеры, которые мы используем в своей работе чаще всего.
Simple Controller — контроллер, который ничего не делает, кроме хранения данных. Мы его используем как папку для удобства хранения тестов, чтобы в них была красивая понятная структура. Также для структурирования тестов можно использовать Transaction Controller, в этом случае при запуске семплеры будут складываться в отдельное дерево в View Results Tree.
If Controller используется, если нужно добавить условие для выполнения шага. Не стоит увлекаться с данным контроллером и использовать его для реализации бизнес-логики в тестах, лучше делать явные отдельные тесты на разные условия.
Например, при написании теста «Стоимость подписки в зависимости от тарифа» не стоит проверять If-ом тариф на X/Y. Тест должен выглядеть как:
Подключаем подписку тарифу X;
Проверяем сумму услуги = 100.00.
А не:
Подключаем подписку случайному тарифу;
Если тариф X, сумма услуги = 100.00;
Если тариф Y, сумма услуги = 500.00. и т. д.
А теперь к более приближённым примерам: если первый запрос не вернул ошибку, то нужно отправить второй. Для этого в первый запрос добавляем JSON Extractor с дефолтным значением, например — NOT_FOUND. Если мы не получим ошибку на первый запрос, то переменная error_code будет равна NOT_FOUND.
После первого запроса вставляем If Controller c выключенным флагом Interpret Condition as Variable Expression ? и добавляем условие:
"${error_code}" == "NOT_FOUND"
В If Controller вкладываем второй запрос, который будет отправлен после выполнения условия.
While Controller применяется для многократного повторения запроса, пока не выполнится определённое условие (вернётся нужный ответ). Например, если нужно отправлять запрос, пока не вернётся значение success_status равное true:
Также While Controller можно использовать, если нужно перебрать список значений и для каждого выполнить одинаковый запрос. Реализация цикла while в языках программирования выглядит примерно так:
let i = 0;
while (i < 3) {
alert( i );
i++;
}
Получается, данный цикл будет выводить 0, затем 1, а затем 2.
В мире Jmeter с While Controller такой цикл будет выглядеть следующим образом:
Определяем переменную counter = 0.
2. Добавляем While Controller с условием counter < 3 (будет работать пока переменная меньше 3).
В JSR223 PostProcessor инкрементируем переменную:
Если сделали всё верно, то в лисенере должны увидеть примерно следующую картину:
ForEach Controller нужен для обработки всех элементов массива или коллекции. На вход принимаются переменные, которые выглядят как var_1, var_2, var_3
и т. д. На выход возвращается переменная, которая приравнивается значению переменной с нужной итерацией.
Пример: Запрос возвращает массив со списком ссылок. Мы хотим проверить отдачу картинок по каждой ссылке.
Для этого добавляем JSON Extractor на все нужные нам ссылки (
Match No. = -1
так сохранятся ВСЕ нужные нам значения в видеvar_name_1,var_name_2
и так далее).
Добавляем ForEach Controller
Заполняем значения в контроллере:
Input Variable Prefix — это имя нашей переменной, которую сохраняли в п.1.
Start Index for loop — значение, с которого начнется цикл, ставим 0.
End Index for loop — значение, до которого будет работать цикл. При сохранении переменной, как мы сделали это в п1.1, JMeter создает автоматически переменную, которая считает кол-во найденных вхождений в jsonpath. Выглядит как
var_name_matchNr
.Output Variable name — имя переменной, которую будем использовать в цикле.
Добавляем HTTP Request с Size Assertion
Важно иметь в виду, что данный контроллер принимает на вход префикс одной переменной. Две и более переменных на вход он не принимает. Иногда есть необходимость в данном контроллере пробегать по нескольким переменным, для этого мы используем счётчики и конструкцию обращения к переменным внутри переменных. Если переменная состоит их двух частей, например VAR_ + ${LAYER}
, то обращаться к ней нужно таким образом:
${_V(VAR{LAYER})}
Пример: Имеем на входе 2 массива переменных: имя хозяина и кличка животного, которые сохранили через JDBC Request в виде: firstName_1,firstName_2,firstName_3,...firstName#
petName_1,petName_2,petName_3,...petName_#
Первую переменную
firstName
мы вставляем в цикл ForEach Controller, как описано в предыдущем примере.Для того чтобы обратиться к переменной
petName
, нужно добавить счётчик:
Настраиваем его и даем ему имя.
Далее в запрос, вложенный в цикл, вставляем переменную в формате
${_V(petName${counter})}
Loop Controller применяется, если нужно отправить запрос определённое кол-во раз. Покажем, как его применять:
Добавляем Loop Controller, в поле Loop Count. вводим число повторов или переменную, которая содержит число.
Добавляем Counter, в нём заполняем начальное значение (Starting Value), шаг, с которым будут перебираться значения (Increment), и название переменной, в которую будет сохраняться номер цикла (Exported Value Name). Переменную можно использовать для перебора переменных, аналогично циклу ForEach Controller —
var_${Counter}
.
Как запускать тесты через терминал, менять окружение и запускать тесты в CI
Запускать тесты в GUI режиме просто — нажать кнопку Start. Но иногда есть необходимость запустить тесты через командную строку (в NON GUI режиме). Чаще всего это применяется при запуске тестов в CI.
Обратимся к официальному мануалу и посмотрим, как запустить тесты через командную строку. В общем виде команда запуска выглядит так:
jmeter -n -t ${JMETER_PATH}/test-plan/${FILE_NAME}.jmx
Тестовых слоёв может быть несколько и на всех надо прогонять тесты. Управлять окружением можно несколькими способами:
Руками менять Basic, пароли и т. д. — очень плохая идея. Спросите почему? Всё просто, нужно понимать риски, что эти данные являются чувствительными. Другими словами, если хранить их в открытом репозитории, то каждый желающий их сможет возыметь;
Вынести все изменяемые данные в User Defined Variables, закомментировать ненужное в переменных системы — идея лучше, имеет право на жизнь;
Назвать все меняющиеся данные с постфиксом с названием слоя, например
DB_URL_DEV
иDB_URL_STAGE
и менять окружение при запуске Jmeter как в GUI, так и в NON GUI режимах — лучшее решение.
Поговорим подробнее про последний вариант. Вы называете переменные DB_URL_DEV
и DB_URL_STAGE
, заносите их в переменные системы (.zprofile
или .bashprofile
). В Jmeter в User Defined Variables обращаетесь к этим переменным таким образом:
${__env(DB_URL_${__P(layer)})}
А сам слой определяете с помощью Jmeter property
jmeter -n -t ${JMETER_PATH}/test-plan/${FILE_NAME}.jmx -Jlayer=${LAYER}
Таким образом:
у вас все переменные лежат в одном месте;
вы одной командой меняете слой, на котором запускаются тесты.
Всё, что описано выше, пригодится при запуске тестов в CI. Ведь тесты не должны ходить только локально, особенно, если у вас большая команда.
Выбрав любой из множества инструментов CI, и найдя к нему интеграцию Allure (например, Jenkins), можно создать билд, который будет запускать ваши тесты. Для этого всё готово — есть docker-compose.yml
, в котором прописаны все действия. Ваша задача — лишь настроить интеграцию CI с Allure. Тесты запускаются в docker. Если есть необходимость, то отправляется в Allure TestOps.
Многопоточность
В какой-то момент тестов стало так много, что их прохождение начало занимать очень много времени и тогда встал вопрос о параллельном запуске тестов.
JMeter, как нагрузочный инструмент, постоянно работает с потоками, для этого настраивается Numbers of threads в тред группе, но для функционального тестирования эта настройка не подойдёт.
Решением для ускорения прогона тестов стало использование нескольких Thread Group.
Далее расскажем принцип использования нескольких тредов. Изначально имеем такую структуру тестов:
.
├── main.jmx
│ ├── Thread Group - 1
│ │ ├── [TF]-FIRST-TF.jmx
│ │ ├── [TF]-SECOND-TF.jmx
│ │ └── [TF]-THIRD-TF.jmx
│ └──
├──
Один тред групп и несколько тестовых фрагментов. Если на выполнение каждого тестового фрагмента уходит по 2 минуты, то прогон всего треда занимает 6 минут. Теперь разобьём тесты по отдельным тредам:
.
├── main.jmx
│ ├── Thread Group - 1
│ │ └── [TF]-FIRST-TF.jmx
│ ├── Thread Group - 2
│ │ └── [TF]-SECOND-TF.jmx
│ ├── Thread Group - 3
│ │ └── [TF]-THIRD-TF.jmx
│ ├── ...
│ │ └── ....
│ └──
├──
В результате наши 6 минут превратились в 2, ну не круто ли?
Теперь потоки будут работать параллельно. Так как они не знают друг о друге ничего, могут мешать друг другу при прогоне тестов. В целом лучше стараться создавать тестовые фрагменты изолированными друг от друга. Если не удаётся этого добиться, то можно пожертвовать параллельностью и указать в настройках «конфликтной» Thread Group задержку запуска.
Иногда можно обойтись без задержек — сдвинуть конфликтный тестовый фрагмент по порядку в пределах его Thread Group так, чтобы к моменту его запуска он ни с кем не конфликтовал.
Если использовать allure-reporter, то данную «конкуренцию» тредов можно увидеть в Allure 2.0. Зайдя во вкладку timeline, можно увидеть каждый тред по отдельности, что за чем шло и т. д. При конкуренции тредов будут видны красные тесты примерно в одно и то же время.
Создавать тред-группы можно до бесконечности, всё ограничивается производительностью вашей локальной машинки (у jmeter, с его возможностями в нагрузочном тестировании, проблем с ресурсностью не будет). С добавлением большого количества тредов скрипт начинает много весить и при открытии файла требуется больше ресурсов, но это того стоит. Для примера, 550 тестов в 17 потоков проходят в среднем за 7 минут. Хотя могли бы проходить 1,5 часа, если бы запускались в одном потоке.
JMeter Allure Reporter
Написать хорошие тесты — это ещё половина дела. Вторая половина дела — сделать полезные и понятные отчёты.
Давайте представим: мы создали запросы, написали пару тестов, запустили локально в GUI и видим:
Вроде всё понятно, что и где упало, знаем куда смотреть. А теперь давайте представим, что автотесты будут ходить не в GUI, а прямо из терминала. Чтобы получить стандартный отчёт от JMeter не в GUI режиме, нужно запускать его с определёнными параметрами:
./apache-jmeter/bin/jmeter -n -t allure-jmeter-example.jmx -l result/result.jtl -j result/jmeter.log -e -o result/report/
Запустим и посмотрим, что имеем в стандартном отчёте:
Видим какие-то ошибки, на каких-то запросах. А понять какая была цепочка запросов, в которой встретилась та или иная ошибка, какие запросы/ответы — уже нереально. Ни понять, ни отловить, ни логов найти по идентификаторам — больно! Поэтому мы начинали задумываться о нормальных, человекочитаемых, привычных для глаз отчётах.
Женим JMeter с Allure
Мы сразу же наметили путь в сторону формата Allure, на это есть пара причин:
Мы используем TMS (Test Management System), которая кушает только формат Allure. И хорошо было бы подружить эти два инструмента между собой;
Формат распространён и гибок, обрабатывается почти везде. Да и плюшек, помогающих анализировать и фильтровать результаты достаточно.
Захотели — cделали! Имея открытое API JMeter и возможность запускать код внутри наших запросов через процессоры, мы написали «репортер», который создаёт нужные данные в формате Allure.
И как говорится — машем волшебной палочкой
и получаем следующее:
Теперь можем наблюдать результаты в знакомом всем формате Allure. Вся цепочка перед глазами, все ошибки, все запросы/ответы. Можем разделять проверки на Epic/Feature/Story, добавлять ссылки, комментарии, теги и кастомные лейблы. Если коротко, то абсолютно всё, что умеет кушать Allure, можно добавить.
Как пример, с добавлением задачи + описания + тега + овнера.
К слову, аттачменты добавляются тут автоматически, кастомные лейблы прописываются в начале наших цепочек и ничего дополнительного настраивать тут не нужно.
Кажется, тема написания всяческих «адаптеров» для Allure уже большинству известна. Описывать тут, как это работает — будет излишним, но, если желающие будут, мы подумаем и напишем :)
В данном случае лишь сошлюсь на наш открытый репозиторий, где описано, как это работает и что нужно у себя сделать, чтобы всё заработало. Там и docker-compose и готовый образ, там и пример со всеми тестами, которые мы частенько используем https://github.com/nonealexq/jmeter-allure-reporting. Используйте у себя, ставьте звёздочки, следите за изменениями. С возникшими трудностями пишите issues. За репозиторием следим и постоянно обновляем.
Итого
Единственная проблема использования JMeter как инструмента для автоматизации функционального тестирования и НЕ локального прогона — отчёты. Они были скудными, не давали никакого понимания куда бежать и что смотреть. Но JMeter, опенсорс, и мы — сделали всё, чтобы эту проблему решить. Теперь всё красиво, всё понятно, и интегрировано с TMS, что даёт нам ещё и статистику и историчность всего происходящего.
В статье мы постарались описать необходимый минимум для вкатывания в написание тестов на JMeter. Если вы давно откладывали автоматизацию тестирования, потому что не было времени учить языки программирования, то теперь у вас есть инструмент, с которым вы можете написать свой первый автотест уже сегодня.