В этой статье поговорим о Нагрузочном тестировании при помощи JMeter-Java-Dsl и реализуем наш первый нагрузочный тест для API с генерацией динамических значений.

Документация JMeter Guide и GitHub с примером кода из статьи.
Предыстория
Была идея выбрать инструмент для нагрузочного тестирования API, чтобы можно было разрабатывать скрипт на ЯП Java и не использовать при этом графический интерфейс, после изучения остановился на Gatling и JMeter и вот о последнем захотелось рассказать в статье, как всё было и что из этого получилось.
Целью для нагрузочного тестирования было выбрано приложение для мониторинга использования газа и воды, где есть только API (ссылка на приложение — utilitiesMonitor). Простое приложение Spring-Boot для мониторинга измерения коммунальных услуг. Приложение имеет две конечные точки REST:
1. Сохранение новых измерений для конкретного пользователя:
POST /measurements
{
"userId":1,
"gas":45.4,
"coldWater":45.2,
"hotWater":557
}2. Получение историй измерений конкретного пользователя:
GET /users/{userId}/measurementsЧтобы получить некоторую историю, нужно сохранить измерения для любого userId (по POST запросу) и использовать то же самое userId в запросе GET. Результаты будут расположены в порядке убывания по дате и времени сохранения измерения.
Итак, поехали
Первое, что мы сделаем, чтобы начать реализовывать тест по нагрузке, пойдём в документацию (вот она — JMeter Guide), но хочется именно живого примера, шаг за шагом, который можно взять и переиспользовать под свои нужды.
Что нам потребуется для этого:
>> IDE, Java и сборщик, возьмем для этого IntelliJ IDEA, Java17 и Maven,
Создадим проект (отпустим детали создания проекта ) и для начала нам нужна еще зависимость Jmeter-Java-Dsl на момент написания статьи последняя версия 1.19
<dependency>
<groupId>us.abstracta.jmeter</groupId>
<artifactId>jmeter-java-dsl</artifactId>
<version>1.19</version>
<scope>test</scope>
</dependency>В качестве инфраструктуры тестирования будем использовать JUnit 5 и библиотеку AssertJ для написания утверждений.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>а для генерации данных JavaFaker.
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>*при генерации данных можем использовать как диапазон валидных значений, так и не валидных для негативных сценариев.
Тестовый План
Сделаем Java class и дадим ему название, например: PerformanceTest , где будет наш тестовый план для нагрузочного тестирования API. В целом, JMeter-Java-Dsl предоставляет мощный и гибкий способ создания планов тестирования, как это будет выглядеть, дальше рассмотрим подробнее, Что ? Где ? Когда ?

Наш тестовый план начнём с threadGroup, где добавим количество потоков, пусть будет 5 (одновременных виртуальных пользователей) и 10 итераций, чтобы было удобнее для восприятия, вынесем в переменные THREADS и ITERATIONS, а в httpSampler передаём адрес нашего сервиса, а так как это Post запрос, у которого есть "тело", нам нужно его передать, чтобы каждый наш запрос был уникальным, мы можем использовать разные, н�� заранее определённые данные для каждого запроса, например, использовать разные userId, gas, coldWater, hotWater (из заданного набора) в каждом запросе. Этого можно легко достичь, используя предоставленный элемент csvDataSet. Например, имея csv файл, подобный этому:
USERID,GAS,COLDWATER,HOTWATER
1,11.1,42.1,111
10,11.2,42.3,112и добавив в наш тестовый план csvDataSet("path to csv"), а уже в "теле" запросе передавать переменные ${USERID}, ${GAS}, ${COLDWATER}, ${HOTWATER}.
Для уникальных значений можем использовать и так называемый Счётчик (Counter), который обеспечивает простые средства для автоматического увеличения
значений, которые можно использовать в запросах. Подробнее можно почитать здесь
Но нам нужно больше гибкости и масштабируемости, поэтому на следующем шаге в .post(s -> buildRequestBody() мы будем формировать динамические значения в "теле" запроса. Чтобы использовать собственную логику используем jsr223preProcessor, более подробно о jsr223preProcessor. Самый лаконичный и рекомендуемый вариант — через лямбда выражения.
TestPlanStats stats = testPlan(
threadGroup(THREADS, ITERATIONS,
httpSampler("http://localhost:8080/measurements")
.method(HTTPConstants.POST)
.post(s -> buildRequestBody(), ContentType.APPLICATION_JSON)Если "провалимся" в метод buildRequestBody() увидим, что здесь мы формируем "тело" запроса для нашего нагрузочного теста, в конкретном примере для работы с данными используем JSONObject, но можем использовать любой другой удобный подход, чтобы каждый запрос был уникальным, используем JavaFaker для генерации передаваемых значений в json. Документация JavaFaker.
Так это выглядит:
public static String buildRequestBody() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("userId", getRandomInteger());
jsonObject.put("gas", getRandomDouble());
jsonObject.put("coldWater", getRandomDouble());
jsonObject.put("hotWater", getRandomDouble());
return jsonObject.toJSONString();
}Но так как полученные после сохранения новых измерений на методе POST/measurements данные нам нужно использовать дальше в нашем сценарии, мы должны данные извлечь. Извлекать мы будем userId из ответа, используя jsonExtractor в качестве дочернего элемента httpSampler, потому что именно userId нам понадобится для следующего запроса. Для этого дадим название переменной: не будем называть переменную абстрактно, назовём userIdVariable и передадим извлекаемое значение userId. По умолчанию jsonExtractor использует JMeter JSON JMESpath Extractor и, как следствие, JMESpath в качестве языка запросов. Подробнее можно почитать здесь.
Нелишним будет сразу проверить, что данные в ответе соответствуют тому, что мы ожидаем через jsonAssertion, просто проверим, что в ответе у нас есть поле dateTime, подробнее об утверждениях тут. Утверждения, такие как jsonAssertion, рекомендуются именно для работы с json, да это и так понятно из названия. Чтобы использовать утверждения, нужно также добавить его в качестве дочернего элемента httpSampler и идём дальше по нашему тестовому плану.
.children(jsonExtractor("userIdVariable", "userId"))
.children(jsonAssertion("dateTime"))Итак, данные извлекли, теперь нам нужно получить историю измерений через GET /users/{userId}/measurements, куда передаём полученный userId, чтобы использовать полученное значение, можно ссылаться на переменную, используя ${variableName}, в нашем случае это ${userIdVariable} и так же, как ранее через jsonAssertion выполним проверку, что в массиве ответа у нас присутствует userId, так как в ответ нам приходит именно массив.
httpSampler("http://localhost:8080/users/${userIdVariable}/measurements")
.method(HTTPConstants.GET)
.contentType(ContentType.APPLICATION_JSON)
.children(jsonAssertion("[*].userId"))Для получения метрик нашего нагрузочного тестового сценария публикуем показатели в influxDbListener и визуализируем в Grafana, который в конкретном примере запущен у нас локально.
influxDbListener("http://localhost:8086/write?db=jmeter")
.measurement("jmeter")
.application("jmeter")
.token("Token from influxDb")На этом подробно останавливаться не будем, подробнее можно почитать о метриках здесь. А в завершение добавим утверждение для валидации, используя AssertJ, что означает, что этот тест завершится неудачно, если он не получит указанную информацию или если 99-й процентиль времени ответа на запросы будет превышать или равен 5 секундам.
assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));Вот что у нас получилось:
import org.apache.http.entity.ContentType;
import org.apache.jmeter.protocol.http.util.HTTPConstants;
import org.junit.jupiter.api.Test;
import us.abstracta.jmeter.javadsl.core.TestPlanStats;
import java.io.IOException;
import java.time.Duration;
import static com.ipavlov.jmeter.generator.RequestBodyGenerator.buildRequestBody;
import static org.assertj.core.api.Assertions.assertThat;
import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
public class PerformanceTest {
private static final Integer THREADS = 5;
private static final Integer ITERATIONS = 10;
@Test
public void saveAndGetMeasurementsLoadTest() throws IOException {
TestPlanStats stats = testPlan(
threadGroup(THREADS, ITERATIONS,
httpSampler("http://localhost:8080/measurements")
.method(HTTPConstants.POST)
.post(s -> buildRequestBody(), ContentType.APPLICATION_JSON)
.children(jsonExtractor("userIdVariable", "userId"))
.children(jsonAssertion("dateTime")),
httpSampler("http://localhost:8080/users/${userIdVariable}/measurements")
.method(HTTPConstants.GET)
.contentType(ContentType.APPLICATION_JSON)
.children(jsonAssertion("[*].userId"))
), influxDbListener("http://localhost:8086/write?db=jmeter")
.measurement("jmeter")
.application("jmeter")
.token("TOKEN from influxDb")
).run();
assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
}
}Заключение
Таким образом мы реализовали тестовый план, который можно расширять, изменять под наши требования и тем самым давать различную нагрузку на наше приложение и оценивать стабильность работы при различных сценариях, а JMeter-Java-Dsl значительно облегчает создание, выполнение и обслуживание тестов производительности, помогает создавать более читабельные планы тестирования в подходящем формате для git и позволит нам включить тесты в CI/CD, достигнув тем самым более высокого уровня автоматизации.
В статье поделился, как это делал я, если есть более лучшие практики и подходы, не проходите мимо, обязательно делитесь.