Сколько раз мы видели инфраструктуру и методологию тестирования, когда команда не может получить своевременную обратную связь о производительности разрабатываемой системы? Как правило, к тестированию производительности принято относиться как к "проекту по методу водопада", когда мы, тестировщики, готовим тест и запускаем его в продакшн непосредственно перед деплоем. Однако есть лучший способ сделать это, и он заключается в непрерывном тестировании производительности. При правильной имплементации оно (непрерывное тестирование) придает уверенность разработчикам и предупреждает их о любом существенном снижении производительности системы.
В ответ на растущий тренд в области инструментов тестирования производительности и, тому, как наш главный технический директор Roger Abelenda продолжает исследование об этом в данной статье, мы разработали решение с открытым исходным кодом, которое, по нашему мнению, может внести огромный вклад в сообщество разработчиков программного обеспечения. Проект называется "JMeterDSL", и мы намерены показать вам, какую пользу вы сможете из него извлечь.
Что такое JMeterDSL?
JMeterDSL — это новый Java API, который использует преимущества кодирования для создания и выполнения тестов JMeter. Его основная цель — предоставить удобный для программистов API, который позволяет тестировщикам и разработчикам создавать более читабельные планы тестирования в формате, удобном для git. Это особенно рекомендуется для команд, которые используют или хотят начать использовать нагрузочное тестирование в среде CI/CD. Приложение поможет им масштабировать тесты в непрерывном потоке.
Всем тестировщикам, разработчикам и техлидам, желающим углубиться в суть, мы настоятельно рекомендуем посмотреть этот вебинар. В нем София Паламарчук и Melissa Chawla обсуждают основные преимущества этого подхода и то, как он помог Shutterfly имплементировать непрерывное тестирование производительности.
Наличие тестов JMeter в виде кода дает ряд преимуществ, например, делает тестовые сценарии короче, а значит, их быстрее читать, редактировать и поддерживать. Это облегчает процесс модульной разработки скриптов и предоставляет возможность использования встроенной документации. JMeterDSL также предлагает руководство пользователя, которое, помимо представления самой DSL, направлено на сокращение кривой обучения JMeter путем предоставления лучших практик, предупреждений и советов.
Мой первый тест с JMeterDSL, шаг за шагом
Мы покажем, как создать простой тест для сайта Opencart, где пользователь заходит на веб-сайт и выбирает продукт. При этом мы рассмотрим основные функции, которые понадобятся вам при создании сценария теста производительности.
Настройка
В этом примере мы будем использовать Maven. С другими вариантами настройки проекта можно ознакомиться в руководстве пользователя.
Чтобы начать использовать JMeterDSL, достаточно создать проект Maven и включить следующую зависимость в файл pom.xml.
<dependency>
<groupId>us.abstracta.jmeter</groupId>
<artifactId>jmeter-java-dsl</artifactId>
<version>0.28</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>3.21.0</version>
<scope>test</scope>
</dependency>
Запись тестового потока
Обычно в JMeter мы записываем наши тесты с помощью JMeter Recorder или стороннего прокси для фиксации запросов, поскольку это позволяет нам увидеть больше деталей, чем JMeter Test Script Recorder. В этом примере мы будем использовать fiddler, скачать его можно здесь.
После включения прокси мы можем вручную выполнять шаги в браузере и просматривать запросы в fiddler.
После удаления всех запросов к другим хостам и статических ресурсов ( .js, .css и image файлы), вот как выглядит запись:
Следующим шагом будет сопоставление этих запросов с JMeterDSL.
Создание сценария
Давайте начнем с простого сценария, который выполняет одну итерацию одного пользователя, выполняющего GET-запрос к главной странице Opencart.
Как и в JMeter, первым шагом будет создание плана тестирования, в который мы добавим группу потоков в качестве параметра. В группе потоков мы можем определить количество потоков (виртуальных пользователей), итераций и сэмплеров, которые будут выполняться. Для выполнения HTTP-запроса мы используем метод httpSampler
с нужным URL.
import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
import java.io.IOException;
import org.junit.jupiter.api.Test;
public class OpencartTest {
@Test
public void OpencartTest() throws IOException {
testPlan(
threadGroup(1, 1,
httpSampler("http://opencart.abstracta.us")
)
).run();
}
}
Сделав это, мы можем добавить утверждение для валидации того, что 99-й процентиль времени ответа запросов составляет менее 5 секунд. В JMeter это было бы невозможно без установки соответствующих плагинов. (Узнайте больше о понимании ключевых метрик тестирования производительности)
Для этого нам нужно сохранить статистику плана тестирования в переменную, чтобы затем использовать ее для получения различных параметров процесса выполнения. В данном случае мы будем использовать sampleTimePercentile99
, а метод AssertJ isLessThan
.
import static org.assertj.core.api.Assertions.assertThat;
import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
import java.io.IOException;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import us.abstracta.jmeter.javadsl.core.TestPlanStats;
public class OpencartTest {
private final String protocol = "http://";
private final String host = "opencart.abstracta.us";
private final String baseUrl = protocol + host;
@Test
public void OpencartTest() throws IOException {
TestPlanStats stats = testPlan(
threadGroup(1, 1,
httpSampler(baseUrl)
)
).run();
assertThat(stats.overall().sampleTimePercentile99()).isLessThan(Duration.ofSeconds(5));
}
}
Теперь, когда мы запустим тест, если 99-й процентиль времени отклика будет больше или равен 5 секундам, то он завершится неудачей. Чтобы избежать дублирования, мы параметризовали базовый url и протокол.
Добавление заголовков
Мы создали первый запрос get, но ему все еще не хватает заголовков. Чтобы добавить их, можно использовать метод header
с правильным именем и значением.
Взглянув на запись fiddler, видно все заголовки, которые отправляются вместе с этим запросом.
Чтобы добавить их в сэмплер, мы используем метод header("name", "value")
.
httpSampler(baseUrl)
.header("Host", host)
.header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-US,en;q=0.9")
Еще одним вариантом в данном случае является использование констант. Можно импортировать HTTPConstants следующим образом:
import org.apache.jmeter.protocol.http.util.HTTPConstants;
Теперь для имен заголовков используются константы. Например, вот как будет выглядеть заголовок хоста:
.header(HTTPConstants.HEADER_HOST,host)
Добавим заголовки для всех записанных запросов. В конечном итоге группа потоков должна выглядеть следующим образом:
threadGroup(1, 1,
httpSampler(baseUrl)
.header("Host", host)
.header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
.header("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-US,en;q=0.9"),
httpSampler(baseProductsUrl + productQuery)
.header("Host", host)
.header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.header("Referer", baseUrl + "/")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-US,en;q=0.9"),
httpSampler(baseProductsUrl + "/review" + productQuery)
.header("Host", host)
.header("Connection", "keep-alive")
.header("Accept", "text/html, */*; q=0.01")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
.header("X-Requested-With", "XMLHttpRequest")
.header("Referer", baseProductsUrl + productQuery)
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-US,en;q=0.9")
Чтобы упорядочить сценарий, сгруппируем два последних запроса в транзакцию, поскольку они соответствуют одному и тому же действию, и добавим имена к каждому из них, что позволит нам легко идентифицировать эти транзакции/шаги во время выполнения.
Как видите, я также сократил дублирование кода, определив эти переменные:
String baseProductsUrl = baseUrl + "/index.php?route=product/product";
String productQuery = "&product_id=40";
Мы также можем использовать общий элемент httpHeaders с заголовками, которые повторяются в каждом запросе. Как и в JMeter, этот элемент будет взаимодействовать со всеми компонентами на одном уровне, поэтому для создания общего httpHeaders мы должны создать его непосредственно в группе потоков в той же области видимости, что и остальные сэмплеры.
Вот как выглядит группа потоков сейчас:
threadGroup(1, 1,
httpHeaders()
.header("Host", host)
.header("Connection", "keep-alive")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-US,en;q=0.9")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"),
httpSampler("Enter Opencart website", baseUrl)
.header("Upgrade-Insecure-Requests", "1")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"),
transaction("Click product",
httpSampler("Click product - product", baseProductsUrl + productQuery)
.header("Upgrade-Insecure-Requests", "1")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.header("Referer", baseUrl + "/"),
httpSampler("Click product - review", baseProductsUrl + "/review" + productQuery)
.header("Accept", "text/html, */*; q=0.01")
.header("X-Requested-With", "XMLHttpRequest")
.header("Referer", baseProductsUrl + productQuery)
)
)
Добавление утверждений ответа
Утверждения, такие как это, подтверждают, что сервер отвечает так, как ожидается, и проверяют, что разрабатываемый нами скрипт действует так, как должен. Для добавления утверждений мы будем использовать элемент responseAssertion
. Можно использовать такие методы, как containsSubstrings
, equalsToStrings
, containsRegexes
и matchesRegexes
. Чтобы использовать один из них в сэмплере, нужно добавить его в качестве дочернего элемента:
httpSampler("Enter Opencart website", baseUrl)
.children(
responseAssertion().containsSubstrings("<title>Your Store</title>")
)
В приведенном выше примере мы проверяем наличие заголовка в полученном ответе, используя часть HTML страницы.
Добавление таймеров
Таймеры важны для правильного имитирования поведения реального пользователя. Чтобы добавить таймер, мы можем использовать метод uniformRandomTimer(minimumMillis,maximumMillis)
. Этот метод добавляет паузу между выбранными значениями перед выполнением запроса. Если мы хотим, чтобы таймер влиял только на один сэмплер, мы должны добавить его в качестве дочернего элемента. Как и в JMeter, если мы добавим таймер на том же уровне, что и запрос, он будет влиять на каждый запрос в группе потоков.
httpSampler("Enter Opencart website", baseUrl)
.children(
uniformRandomTimer(1000, 2000)
)
Извлечение переменных из ответа
Согласно существующего скрипта мы всегда выбираем один и тот же продукт (id продукта) с веб-страниц в каждом запуске. Чтобы обеспечить динамичность выбора давайте извлечем идентификатор товара из предыдущего ответа.
Для этого мы будем использовать regexExtractor("variableName", "regex")
в качестве дочернего элемента сэмплера. Он работает так же, как JMeter's Regular Expression Extractor, поэтому вы можете изменить и другие параметры, такие как шаблон, значение по умолчанию, номер совпадения и целевое поле.
Мы определим переменную с именем "product". Эта переменная содержит имя продукта, из которого мы будем извлекать ID, и будем использовать его для создания регулярного выражения.
String product = "iPhone";
httpSampler("Enter Opencart website", baseUrl)
.children(
regexExtractor("productId", "product_id=(\\d*)\">" + product)
)
Использование извлеченных переменных
Чтобы использовать извлеченное значение, можно сослаться на переменную, используя "${variableName}"
. Так, для использования переменной productId
при выборе продукта, можно встроить ее, как показано ниже:
String productQuery = "&product_id=${productId}";
Добавление CSV-файлов
А теперь, что если нам нужно, чтобы каждый пользователь выбирал свой продукт? Давайте добавим CSV-файл с 3 различными продуктами и модифицируем группу потоков для работы с 3 пользователями.
Для добавления CSV применяем csvDataSet("csvFile")
. Используем полный или относительный путь от корня проекта.
csvDataSet("./src/test/java/products.csv"),
threadGroup(3, 1,
httpSampler("Enter Opencart website", baseUrl)
.children(
responseAssertion().containsSubstrings("<title>Your Store</title>"),
regexExtractor("productId", "product_id=(\\d*)\">${product}"),
uniformRandomTimer(1000, 2000)
),
transaction("Click product",
httpSampler("Click product - product", baseProductsUrl + productQuery),
httpSampler("Click product review", baseProductsUrl + "/review" + productQuery)
)
)
Отладка
Для отладки добавим элемент resultsTreeVisualizer()
в план тестирования. Если мы включим эту опцию, будет отображаться встроенный в JMeter элемент View Results Tree. Это позволит нам просматривать транзакции в реальном времени во время выполнения сценария, отображая запросы и ответы для каждого из примеров в дополнение к собранным метрикам.
Создание отчетов
Для создания html-отчета, просто добавьте элемент htmlReporter("reportDirectory")
, где reportDirectory
должен быть пустым каталогом. Можно генерировать новую директорию каждый раз следующим образом:
htmlReporter("html-report-" + Instant.now().toString().replace(":", "-"))
Финальные выводы
Мы считаем, что JMeterDSL — это отличный инструмент для всех, с опытом работы в JMeter и программировании или без него. Он значительно облегчает создание, выполнение и обслуживание тестов производительности. Если вы пытаетесь в кратчайшие сроки создать нагрузочные тесты, убедиться в их работоспособности и/или включить их в CI/CD-пайплайн, то в этом случае JMeterDSL будет как нельзя кстати.
Всех желающих приглашаем на открытый вебинар «Стенды для нагрузочного тестирования».