Перейти к основному содержимому

Практикум — API-тестер на Groovy и JMeter

Разработчику

Практикум — API-тестер на Groovy и JMeter

Groovy API Tester — desktop-приложение для ручной проверки REST API. Вы вводите URL, HTTP-метод, заголовки и тело запроса — получаете статус, время ответа, заголовки и тело ответа. Под капотом Apache JMeter работает программно, без графического интерфейса JMeter и без .jmx-файлов.

Практикум показывает Groovy за пределами build.gradle и Jenkinsfile:

Готовый проект можно взять как эталон — каталог GroovyAPITester рядом с другими JVM-проектами. Если Groovy-проект ещё не запускали — сначала первая программа и Gradle Groovy DSL. База по HTTP — HTTP как основа веб-интеграций.

Кому полезен практикум

Разработчику, который уже читает build.gradle, но хочет цельное JVM-приложение на Groovy. Тестировщику — как встроить JMeter в код, а не только в GUI. Архитектору — как отделить UI, модели и транспорт (слои без смешения ответственности).


Что получится

После прохождения практикума у вас будет:

  • Gradle-проект с плагинами groovy и application;
  • модели HttpRequestConfig и HttpResponseResult — DTO без привязки к Swing и JMeter;
  • класс JMeterHttpExecutor — один HTTP-запрос через StandardJMeterEngine;
  • окно ApiTesterFrame с вкладками "Заголовки", "Тело запроса", "Ответ";
  • smoke-тест gradle smokeTest для CI (Jenkins Pipeline);
  • команда gradle run — запуск GUI.
gradle run

Горячая клавиша Ctrl+Enter отправляет запрос без клика по кнопке.


Термины

ТерминПростыми словами
REST APIHTTP-интерфейс сервера — ресурсы по URL, методы GET/POST/PUT/PATCH/DELETE
HTTP-методГлагол запроса — что хотим сделать с ресурсом (прочитать, создать, изменить, удалить)
Заголовок (header)Пара "имя — значение" в HTTP-сообщении — Content-Type, Authorization, Accept
Тело запроса (body)Полезная нагрузка POST/PUT/PATCH — часто JSON
Статус-кодЧисло в ответе — 200 OK, 404 Not Found, 500 Internal Server Error
DTOData Transfer Object — класс только для передачи данных между слоями
POGOPlain Old Groovy Object — класс с полями без лишнего boilerplate
Apache JMeterИнструмент нагрузочного и функционального тестирования — HTTP, JDBC, JMS и др.
HTTPSamplerProxyJMeter-компонент, описывающий один HTTP-запрос (хост, путь, метод, тело)
SampleResultОбъект JMeter с результатом одного sampler — код, время, тело, заголовки
TestPlanКорень JMeter-сценария — контейнер всего плана
ThreadGroupГруппа виртуальных пользователей; у нас — один поток, одна итерация
StandardJMeterEngineПрограммный запуск JMeter без .jmx и без GUI
ListedHashTreeДерево элементов JMeter — иерархия TestPlan → ThreadGroup → Sampler
SwingBuilderGroovy-обёртка над Swing — UI описывают вложенными блоками, как mini-DSL
SwingWorkerФоновая задача Swing — doInBackground() не блокирует EDT
EDTEvent Dispatch Thread — поток, в котором рисуется и обрабатывается GUI
Smoke-тестБыстрая проверка "система жива" — один GET без GUI после сборки

Зачем JMeter, если есть Postman и curl

ИнструментСильная сторонаСлабое место для нашей задачи
PostmanУдобный GUI, коллекции, окруженияНужен отдельный продукт; сложнее встроить в свой код
curlМгновенно из терминалаНет своего GUI; парсинг ответа вручную
HttpURLConnection / HttpClientСтандарт JavaМного boilerplate; редиректы, таймауты, логирование — на вас
JMeter (embedded)Зрелый HTTP-стек, метрики времени, тот же движок, что в нагрузочных тестахТяжёлая зависимость; нужна инициализация JMETER_HOME

Живая теория. Мы собираем не замену Postman для команды QA, а учебный проект на Groovy, где HTTP-движок совпадает с инструментом нагрузочного тестирования. Один и тот же HTTPSamplerProxy потом можно масштабировать до тысяч потоков в .jmx-плане — логика запроса уже знакома.

Альтернатива на чистом Groovy — HttpURLConnection или Java 11+ HttpClient (Простые приложения на Groovy). JMeter оправдан, если вы уже работаете с JMeter в CI или хотите единый стек "ручной запрос → нагрузочный сценарий".


Архитектура приложения

СлойКлассОтветственность
Точка входаMainLook-and-feel ОС, запуск окна на EDT
UIApiTesterFrameФорма, парсинг заголовков, отображение ответа
HTTPJMeterHttpExecutorИнициализация JMeter, сборка TestPlan, выполнение
МоделиHttpRequestConfig, HttpResponseResultDTO запроса и ответа

Живая теория. Разделение на слои — тот же принцип, что в ООП и тестировании:

  • UI не знает про HTTPSamplerProxy — только про HttpRequestConfig / HttpResponseResult;
  • JMeterHttpExecutor не знает про Swing — его можно вызвать из SmokeTest, Spock или Jenkins;
  • модели не зависят ни от Swing, ни от JMeter — удобная точка для unit-тестов.

Поток данных при нажатии "Отправить":

  1. ApiTesterFrame читает поля формы → собирает HttpRequestConfig.
  2. SwingWorker вызывает executor.execute(config) не в EDT.
  3. JMeterHttpExecutor строит дерево JMeter, запускает engine, возвращает HttpResponseResult.
  4. done() на EDT обновляет метки и текстовые области.

Анатомия HTTP-запроса

Перед кодом полезно держать в голове структуру сообщения (HTTP — основа):

POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{"name": "Alice"}
ЧастьГде в нашем приложении
Метод + путьmethodCombo + URL (путь и query из URI)
Заголовкивкладка "Заголовки" → HeaderManager
Теловкладка "Тело запроса" → HTTPSamplerProxy.postBodyRaw
Статус и тело ответавкладка "Ответ" ← SampleResult

Живая теория. URL https://api.example.com:8443/v1/users?page=2 JMeter разбирает на части:

  • protocolhttps;
  • domainapi.example.com;
  • port8443 (или 443/80 по умолчанию);
  • path/v1/users?page=2.

Если передать только api.example.com/users без схемы — URI.create не даст валидный запрос; мы явно проверяем scheme и host в execute().


Требования


Шаг 0 · Структура проекта

Создайте каталог GroovyAPITester и дерево файлов:

GroovyAPITester/
├── build.gradle
├── settings.gradle
├── gradlew / gradlew.bat # после gradle wrapper
└── src/main/
├── groovy/apitester/
│ ├── Main.groovy
│ ├── SmokeTest.groovy
│ ├── model/
│ │ ├── HttpRequestConfig.groovy
│ │ └── HttpResponseResult.groovy
│ ├── jmeter/
│ │ └── JMeterHttpExecutor.groovy
│ └── ui/
│ └── ApiTesterFrame.groovy
└── resources/jmeter/bin/
└── jmeter.properties

Файл settings.gradle:

rootProject.name = 'GroovyAPITester'

Разбор:

  • rootProject.name — имя артефакта в build/libs/ и в installDist;
  • каталог src/main/groovy — стандарт Gradle для Groovy (Gradle Groovy DSL — первая сборка), аналог src/main/java;
  • пакет apitester.model, apitester.ui, apitester.jmeterразные причины изменения (данные / интерфейс / транспорт);
  • resources/jmeter/bin/jmeter.properties — попадает в classpath; JMeter находит "домашний каталог" без установки JMeter на машину.

Шаг 1 · build.gradle

Код ITЗагрузка примера кода…

Разбор build.gradle

БлокНазначение
id 'groovy'Компиляция .groovy из src/main/groovy
id 'application'Задачи run, installDist, mainClass
groovy-swingSwingBuilder для UI
groovy-jsonJsonSlurper, JsonOutput.prettyPrint
ApacheJMeter_coreДвижок, утилиты, JMeterUtils
ApacheJMeter_httpHTTPSamplerProxy, HTTP-протокол
ApacheJMeter_componentsHeaderManager, listeners
log4j + slf4jJMeter пишет через SLF4J; без binding — No SLF4J providers
smokeTestОтдельная задача для CI — один GET без GUI
-Dfile.encoding=UTF-8Кириллица в UI и в JSON без "кракозябр"

Разбор фрагмента:

  • def groovyVersion = '3.0.20' — локальная переменная в скрипте Gradle; GString "${groovyVersion}" подставляет версию в координаты Maven (GString).
  • Блок application { mainClass = ... } — плагин application создаёт задачу run, которая запускает static void main указанного класса.
  • tasks.withType(JavaExec).configureEach — правило для всех JavaExec-задач, включая run и smokeTest.
  • tasks.register('smokeTest', JavaExec) — кастомная задача без отдельного .gradle-файла; удобно вызывать из Jenkins или Shared Library.

Живая теория. Файл build.gradle — Groovy-скрипт с делегированием замыканий (Основы языка Groovy). Когда вы пишете dependencies { implementation ... }, Gradle передаёт closure объекту DependencyHandler, и вызов implementation резолвится у делегата — не в глобальной области. Тот же механизм в application { } и tasks.register(...) { }.


Шаг 2 · jmeter.properties

Файл src/main/resources/jmeter/bin/jmeter.properties:

# Minimal JMeter configuration for embedded use
jmeter.save.saveservice.output_format=xml
jmeter.save.saveservice.response_data=true
jmeter.save.saveservice.samplerData=true
jmeter.save.saveservice.requestHeaders=true
jmeter.save.saveservice.response_headers=true

Разбор:

  • output_format=xml — формат сохранения результата sampler (для embedded достаточно xml);
  • response_data=true — в SampleResult попадает тело ответа; без флага UI покажет пустое поле;
  • samplerData=true — данные исходящего запроса (удобно для отладки);
  • requestHeaders / response_headers — заголовки в обе стороны.

Живая теория. У desktop-JMeter "дом" — каталог JMETER_HOME с bin/jmeter.properties. В embedded-режиме мы подменяем его минимальным деревом в resources:

classpath:jmeter/bin/jmeter.properties → JMETER_HOME = classpath:jmeter

Код в ensureInitialized() вычисляет путь: propsFile.parentFile.parentFile — поднимается от bin/ к корню jmeter/. Поэтому структура каталогов жёсткая — не кладите jmeter.properties в корень resources без bin/.


Шаг 3 · Модели запроса и ответа

HttpRequestConfig

package apitester.model

class HttpRequestConfig {

String url
String method = 'GET'
Map<String, String> headers = [:]
String body = ''
int connectTimeoutMs = 10_000
int responseTimeoutMs = 30_000

}

Разбор:

  • String url — полный URL со схемой; парсинг — в JMeterHttpExecutor, не в модели;
  • method = 'GET' — значение по умолчанию для Groovy POGO (ООП);
  • headers = [:] — пустая map-литерала (типы — Map);
  • 10_000 — numeric separator, как в Java 7+;
  • таймауты в миллисекундах — connectTimeout (TCP) и responseTimeout (ожидание ответа) на sampler.

HttpResponseResult

Код ITЗагрузка примера кода…

Разбор:

  • success — флаг JMeter sample.successful (HTTP 4xx/5xx могут быть successful=false в зависимости от настроек);
  • statusCode == 0 — договорённость "HTTP-ответа не было" (ошибка URL, JMeter, сеть);
  • именованные аргументы конструктора — new HttpResponseResult(success: false, ...) без порядка параметров;
  • фабрика error(String) — единый способ вернуть ошибку из execute() без дублирования полей.

Живая теория. DTO намеренно без методов бизнес-логики. parseHeaders живёт в UI, сбор sampler — в JMeterHttpExecutor. Так SmokeTest и Spock-тесты не тянут Swing. Это тот же приём, что Java-record или Kotlin data class для границ слоёв (Groovy и Java).


Шаг 4 · JMeterHttpExecutor

Создайте src/main/groovy/apitester/jmeter/JMeterHttpExecutor.groovy.

4.1 Инициализация JMeter

Код ITЗагрузка примера кода…

Разбор:

  • volatile boolean initialized — видимость флага между потоками после инициализации;
  • synchronized на методе — только один поток выполняет инициализацию JMeter;
  • classLoader.getResource(...) — файл из src/main/resources, упакованный в JAR;
  • new File(jmeterHomeUrl.toURI()) — корректный путь и для файловой системы IDE, и для JAR;
  • setJMeterHome + loadJMeterProperties — обязательная пара перед первым engine.run().

4.2 Метод execute

Код ITЗагрузка примера кода…

Разбор:

  • ранний return HttpResponseResult.error(...)Railway-oriented стиль без глубокой вложенности if/else;
  • LoopController с loops = 1 — ровно одна итерация sampler;
  • numThreads = 1 — один виртуальный пользователь; для ручного тестера достаточно;
  • .tap { addArgument('API_TEST', 'true') } — Groovy-идиома (Синтаксические конструкции Groovy) — настроить Arguments и вернуть тот же объект;
  • engine.exit() в finally — освободить ресурсы engine после каждого запроса;
  • sample.time — время ответа в миллисекундах; bytesAsLong — размер тела.

Живая теория — дерево JMeter. ListedHashTreeконтракт "кто чьим родителем является". Минимальный сценарий одного запроса:

TestPlan
└── ThreadGroup
└── LoopController
└── HTTPSamplerProxy
├── HeaderManager (опционально)
└── CaptureResultCollector

В GUI JMeter вы кликаете правой кнопкой и "Add" — здесь то же самое вызовами tree.add(...). Для нагрузочного теста увеличивают numThreads и loops; код buildSampler остаётся.

4.3 HTTPSampler и заголовки

Код ITЗагрузка примера кода…

Разбор:

  • тернарный оператор для порта — явный порт в URL или 443/80 по схеме;
  • rawPath / rawQuery — path и query без перекодирования; JMeter сам собирает строку запроса;
  • method.toUpperCase() in ['POST', 'PUT', 'PATCH'] — оператор in для списка (операторы);
  • postBodyRaw = true — тело "как есть", не application/x-www-form-urlencoded;
  • addArgument('', config.body) — JMeter-к convention для raw body;
  • equalsIgnoreCase('Content-Type') — не дублировать Content-Type, если пользователь задал вручную;
  • последняя строка buildHeaderManager — Groovy возвращает значение последнего выражения (особенности).

4.4 Перехват результата

private static class CaptureResultCollector extends ResultCollector {

volatile SampleResult capturedResult

@Override
void sampleOccurred(SampleEvent event) {
capturedResult = event.result
super.sampleOccurred(event)
}
}
}

Разбор:

  • ResultCollector — listener JMeter; по умолчанию пишет в файл или агрегатор;
  • переопределение sampleOccurred — перехватить SampleResult в память для UI;
  • volatile — другой поток (worker Swing) прочитает результат после engine.run().

Вспомогательные методы parseStatusCode, formatRequestHeaders, waitForEngine — в полном файле эталонного проекта; waitForEngine опрашивает engine.isActive() с паузой 50 ms до 60 s, чтобы не вернуть пустой результат раньше времени.


Шаг 5 · ApiTesterFrame

Файл src/main/groovy/apitester/ui/ApiTesterFrame.groovy.

5.1 Каркас окна

Код ITЗагрузка примера кода…

Разбор:

  • extends JFrame — главное окно Swing; Groovy генерирует вызовы сеттеров (title =, minimumSize =);
  • поля виджетов объявлены заранее — в SwingBuilder им присваивают ссылку (urlField = textField(...));
  • setLocationRelativeTo(null) — центр экрана;
  • один экземпляр JMeterHttpExecutor на окно — инициализация JMeter переиспользуется.

Живая теория — SwingBuilder как DSL. Вложенный блок:

panel {
gridBagLayout()
label(text: 'URL:', gridx: 0, gridy: 0)
urlField = textField(text: 'https://httpbin.org/get', gridx: 1, gridy: 0)
}

SwingBuilder ставит closure.delegate = builder; вызов label(...) уходит в builder — тот же delegate, что dependencies { implementation ... } в Gradle (Основы языка Groovy). Именованные параметры gridx, fill — Map-аргументы Groovy.

5.2 Компоновка экрана

В buildContent():

  • NORTH — URL, комбобокс метода (GET … OPTIONS), кнопка "Отправить";
  • CENTERtabbedPane с вкладками "Заголовки", "Тело запроса", "Ответ";
  • SOUTH — indeterminate progressBar на время запроса.

Заголовки по умолчанию:

Accept: application/json
User-Agent: GroovyAPITester/1.0

Правила ввода заголовков:

  • формат Имя: значение, одна пара на строку;
  • строки, начинающиеся с #, — комментарии;
  • пустые строки пропускаются.

5.3 Отправка запроса в фоне

Код ITЗагрузка примера кода…

Разбор:

  • urlField.text?.trim() — safe navigation (Groovy и Java); null если виджет пуст;
  • selectedItem as String — явное приведение из Object комбобокса;
  • bodyArea.text ?: '' — Elvis-оператор: null → пустая строка (операторы);
  • doInBackground()запрещено трогать Swing; только executor.execute;
  • get() в done() — результат worker или исключение;
  • setUiBusy(false) в finally — кнопка снова активна даже при ошибке.

Живая теория — EDT. Swing однопоточный для UI: Event Dispatch Thread рисует кнопки и обрабатывает клики. Долгий HTTP на EDT блокирует отрисовку — окно "не отвечает". SwingWorker — стандартный паттерн Java/Swing:

МетодПотокМожно трогать UI
doInBackground()workerнет
done()EDTда

JMeter внутри execute() тоже создаёт потоки — поэтому не вызывайте execute() из EDT напрямую.

5.4 Парсинг заголовков и JSON

Код ITЗагрузка примера кода…

Разбор:


Шаг 6 · Main.groovy

Код ITЗагрузка примера кода…

Разбор:

  • SwingUtilities.invokeLater { } — постановка задачи на EDT; main может быть вызван не из EDT;
  • getSystemLookAndFeelClassName() — нативный вид Windows/macOS/Linux вместо Metal;
  • new ApiTesterFrame().visible = true — Groovy-сеттер для setVisible(true) (особенности);
  • пустой catch — если L&F недоступен, остаётся дефолтный; приложение всё равно стартует.

Шаг 7 · SmokeTest

Код ITЗагрузка примера кода…

Разбор:

  • args ? args[0] : '...' — Groovy truthiness: пустой массив аргументов → дефолтный URL;
  • GString в println — подстановка полей результата;
  • System.exit(1) — ненулевой код для CI; Jenkins/GitHub Actions помечают stage как failed (Jenkins Pipeline — первый Jenkinsfile).

Запуск:

gradle smokeTest
gradle smokeTest --args="https://httpbin.org/post"

Живая теория. Smoke-тест проверяет цепочку целиком — classpath, JMeter init, сеть, парсинг ответа — без поднятия GUI. Его ставят после gradle build в pipeline рядом с unit-тестами (уровни тестирования).


Запуск и проверка

Первый запуск

cd GroovyAPITester
gradle wrapper
gradle run

Windows:

.\gradlew.bat run

Пример POST-запроса

  1. URL — https://httpbin.org/post
  2. Метод — POST
  3. Тело:
{"name": "test"}
  1. Кнопка "Отправить" или Ctrl+Enter

Ожидайте статус 200 и JSON, где поле json повторяет отправленное тело — httpbin эхо-сервис для отладки клиентов.

Сборка дистрибутива

gradle installDist

Исполняемые скрипты — build/install/GroovyAPITester/bin/GroovyAPITester (.bat на Windows). Дистрибутив включает JAR и classpath — JVM на целевой машине обязательна (JVM и байт-код).


Groovy-идиомы в проекте

Место в кодеИдиомаРаздел
HttpRequestConfigPOGO, defaults, [:]Типы данных и объявление переменных
new HttpResponseResult(success: false, ...)Named args конструктораОбъектно-ориентированное программирование в Groovy
.tap { addArgument(...) }Настройка объекта in-placeСинтаксические конструкции Groovy
method in ['POST', 'PUT']in для коллекцийОператоры и выражения в Groovy
SwingBuilder nested blocksDelegate / DSLОсновы языка Groovy
JsonSlurper / JsonOutputJSON без JacksonПростые приложения на Groovy
urlField.text?.trim()Safe navigationGroovy и Java — совместимость и отличия
body ?: ''Elvis-операторОператоры и выражения в Groovy
последнее выражение методаНеявный returnОсобенности и расширения языка Groovy

Частые ошибки

СимптомВероятная причинаЧто проверить
Не найден jmeter.propertiesФайл не в src/main/resources/jmeter/bin/Путь и задача processResources
No SLF4J providersНет log4j-slf4j2-implБлок logging в build.gradle
UI "зависает" на запросеHTTP выполняется в EDTОбернуть в SwingWorker
Пустое тело ответаsaveservice.response_data=falsejmeter.properties
SSL / certificate errorsСамоподписанный сертификатTrust store JMeter или тестовый стенд
MissingPropertyException в UIЗабыли urlField = textField(...)Присвоение полям в SwingBuilder
POST без тела на сервереНет Content-TypebuildHeaderManager добавляет JSON
mainClass not foundРасхождение package и Gradleapplication.mainClass (Gradle Groovy DSL — первая сборка)
Кракозябры в UIНеверная кодировка-Dfile.encoding=UTF-8 в JavaExec

Расширения по желанию

  1. Spock-тест на parseHeaders с таблицей where:Spock — первая спецификация.
  2. История запросов — последние URL в Preferences или JSON-файле (работа с файлами).
  3. Экспорт в .jmx — сохранить sampler как JMeter-план для нагрузочного теста.
  4. Basic Auth — поля login/password → заголовок Authorization: Basic ....
  5. Stage smokeTest в CI — Shared Library, DevOps.
  6. @CompileStatic на parseHeaders — если профилируете hot path (Особенности и расширения языка Groovy).

Связь с разделом Groovy

ЗадачаМатериал
Gradle, wrapper, runGradle Groovy DSL
Замыкания, delegateОсновы — §11
JSON в скриптахПростые приложения
Тесты моделейSpock
CI со smoke-тестомJenkins Pipeline
Игра на GroovyFastJ
HTTP-теорияHTTP — основа
Тестирование APIAPI-тестирование
Нагрузка и JMeterВиды тестирования

Что попробовать

  1. Метод HEAD — тело ответа пустое, заголовки присутствуют.
  2. Spock-тест — parseHeaders('A: 1\n# comment\nB: 2')[A:'1', B:'2'].
  3. Stage gradle smokeTest в Jenkinsfile репозитория.
  4. Сравнить время ответа с curl — curl -w '%{time_total}\n' -o NUL -s URL (Windows) или -o /dev/null (Linux/macOS).
  5. Запрос с намеренно неверным URL — убедиться, что UI показывает HttpResponseResult.error, а не stack trace.

Дальше

Spock — первая спецификация · Jenkins Pipeline · Gradle Groovy DSL · Итоги