Сколько раз мы видели инфраструктуру и методологию тестирования, когда команда не может получить своевременную обратную связь о производительности разрабатываемой системы? Как правило, к тестированию производительности принято относиться как к "проекту по методу водопада", когда мы, тестировщики, готовим тест и запускаем его в продакшн непосредственно перед деплоем. Однако есть лучший способ сделать это, и он заключается в непрерывном тестировании производительности. При правильной имплементации оно (непрерывное тестирование) придает уверенность разработчикам и предупреждает их о любом существенном снижении производительности системы.

В ответ на растущий тренд в области инструментов тестирования производительности и, тому, как наш главный технический директор 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 будет как нельзя кстати.


Всех желающих приглашаем на открытый вебинар «Стенды для нагрузочного тестирования».

Комментарии (0)