Что пишут в блогах

Подписаться

Что пишут в блогах (EN)

Разделы портала

Онлайн-тренинги

.
Как мы приручили JMX-файл на 50 000 строк: декомпозиция JMeter-тестов для нормального code review
07.10.2026 00:00

Автор: Макарий Петров

Работая в Сбере, я столкнулся с тем, что общепринятым инструментом для функционального тестирования в моем трайбе был JMeter. Нравится ли мне это? Вопрос второстепенный. Приходилось работать с тем, что есть. По мере того как разрастались наши компоненты и их функциональность - разрастались и JMeter-тесты. Если кто не сталкивался - вся логика JMeter-тестов описана в файле с расширением .jmx. По сути это XML-файл, содержащий в себе всю логику тестов: вызовы endpoints, проверки JSON и прочая логика. И если подойти к этому вопросу без знания, то можно столкнуться с такой же проблемой, как у нас: файл разросся до 50 000+ строк и вносить/ревьюить/поддерживать его стало крайне сложно. Я нашёл способ уменьшить размер файла в 10 раз. Давайте же вместе распилим этот монолит)



Как выглядел процесс код-ревью


Давайте для начала немного раскрою проблему. Что же плохого в файле на 50 000 строк?

  • Он не помещался в оперативную память моего ноутбука: поиск и редактирование по файлу работали медленно

  • При одновременной работе с JMeter-тестами двумя разными разработчиками случались противные merge-конфликты

  • PR-ревью превратилось в формальность - никто не способен адекватно проверить diff на 3 000 строк XML

  • JSR223 Groovy и SQL скрипты в JMeter не имеют подсказок и подсветки синтаксиса - хотелось бы работать с ними напрямую в IDE

Результат в числах

  • Размер JMX-файла уменьшился до 5 000 строк

  • Вся логика тестов выделена в отдельные файлы

  • Тестирование одного эндпоинта требует в 3 раза меньше строк: с 900 до 300

А теперь давайте поговорим о способах правильно структурировать JMeter-тесты, чтобы добиться такого же результата.


Что делать с SQL-скриптами?

Иногда для тестирования функциональности требуется подготовить таблицы в БД: добавить сущности, которые необходимы для тестов. Для этого в JMeter есть специальная нода - JDBC Request. И у неё есть некоторые минусы:

  • SQL-код этой ноды хранится в JMX-файле

  • У SQL нет подсказок и подсветки синтаксиса в интерфейсе JMeter

В идеале нам хотелось бы вынести эти SQL-скрипты в отдельные .sql файлы, которые можно было бы редактировать в IDE, пользуясь всеми её благами. Небольшой и быстрый гуглинг подсказывает нам, что в JMeter есть отличная встроенная функция __FileToString:

Использование функции FileToString


Однако у этой функции есть одна проблема - она не раскрывает автоматически переменные JMeter из vars. И если раньше мы могли написать вот так:

SQL запрос с переменной из vars


То теперь не можем, и переменная vGeneratedId автоматически не подставится в запрос.

Было решено написать собственный скрипт, который автоматически раскрывал бы переменные, и нам не пришлось бы переписывать все SQL-запросы. Вот сам скрипт:

import groovy.sql.Sql
import java.util.regex.Pattern

// ============================================================
// JMeter JSR223 Sampler - Universal SQL Executor
// ============================================================
// Parameters (через пробел):
// args[0] = JDBC URL (jdbc:postgresql://host:5432/db)
// args[1] = DB username
// args[2] = DB password
// args[3] = путь до .sql файла (можно с пробелами)
//
// Результат пишется в JMeter vars:
// sql_result_count - кол-во строк
// sql_col_count - кол-во колонок
// sql_col_<N> - имя колонки N (1-based)
// sql_<row>_<col> - значение (1-based, например sql_1_1)
// sql_result - первое значение (scalar shortcut)
// ============================================================

def jdbcUrl = args[0]
def dbUser = args[1]
def dbPass = args[2]
def sqlFile = args[3..args.length - 1].join(" ")

// --- Читаем SQL из файла ---
def rawSql = new File(sqlFile).getText("UTF-8")

// --- Подставляем ${varName} -> vars.get("varName") ---
def resolved = rawSql.replaceAll(/\$\{(\w+)\}/) { fullMatch, varName ->
def value = vars.get(varName)
if (value == null) {
log.warn("Variable '${varName}' not found in JMeter vars, leaving as-is")
return fullMatch
}
return value
}

log.info("Executing SQL:\n${resolved}")

// --- Подключаемся и выполняем ---
def sql = Sql.newInstance(jdbcUrl, dbUser, dbPass, "org.postgresql.Driver")

try {
def trimmed = resolved.stripIndent().trim()
def isSelect = trimmed.toUpperCase() =~ /^\s*(SELECT|WITH|VALUES)\b/

if (isSelect) {
// --- SELECT / WITH / VALUES - ожидаем ResultSet ---
def rows = sql.rows(resolved)

if (rows == null || rows.isEmpty()) {
vars.put("sql_result_count", "0")
vars.put("sql_col_count", "0")
vars.put("sql_result", "")
log.info("SQL returned 0 rows")
return
}

def colNames = rows[0].keySet().toList()
vars.put("sql_result_count", String.valueOf(rows.size()))
vars.put("sql_col_count", String.valueOf(colNames.size()))

colNames.eachWithIndex { name, idx ->
vars.put("sql_col_${idx + 1}", name)
}

rows.eachWithIndex { row, rowIdx ->
colNames.eachWithIndex { col, colIdx ->
def val = row[col]
vars.put("sql_${rowIdx + 1}_${colIdx + 1}", val != null ? val.toString() : "")
}
}

def firstVal = rows[0][colNames[0]]
vars.put("sql_result", firstVal != null ? firstVal.toString() : "")
log.info("SQL returned ${rows.size()} row(s), ${colNames.size()} col(s)")

} else {
// --- DML (INSERT/UPDATE/DELETE/MERGE и т.д.) ---
def affected = sql.executeUpdate(resolved)
vars.put("sql_result_count", "0")
vars.put("sql_col_count", "0")
vars.put("sql_result", "")
vars.put("sql_affected_rows", String.valueOf(affected))
log.info("DML executed, affected rows: ${affected}")
}

} catch (Exception e) {
log.error("SQL execution failed: ${e.message}", e)
throw e
} finally {
sql.close()
}

А использовать его можно вот таким образом: нам понадобится нода JSR223 Sampler, в параметры которой мы передаём URL подключения к БД, логин, пароль и относительный путь до .sql файла со скриптом.

Использование скрипта выполнения sql с подстановкой переменных


На этом вынос SQL в отдельные файлы завершён.


Вынос Groovy-скриптов из JSR223-элементов

Данный пункт достаточно очевиден, но хотелось бы его подсветить. Все ноды, которые начинаются с JSR223..., мы выносим в отдельные .groovy файлы. Все скрипты теперь не inline, а лежат в отдельных файлах. Работаем мы с ними напрямую в IDE, а не в интерфейсе JMeter.

Важный момент: при использовании внешних скриптов обязательно ставьте галочку «Cache compiled script if available» в настройках JSR223-элемента. Без неё JMeter будет перекомпилировать Groovy-скрипт при каждом вызове, что заметно просадит производительность.

Также, если JMeter-тестов несколько, то общие утилиты на Groovy можно вынести в отдельные файлы. Например, скрипт по запуску SQL-скриптов, указанный выше, лежит в папке common и доступен всем JMeter-тестам.


Разделение Thread Group по разным JMX-файлам

Оказывается, кусочки JMX-файла можно вынести в разные файлы, указав на них ссылки в главном JMX. Как это делается?

Конкретно в моём проекте тестирование каждого домена было вынесено в отдельный Thread Group. Достаточно было выделить всё, что вложено в каждый Thread Group, и нажать ПКМ → Save as Test Fragment, выбрав путь, где будет лежать логика этой Thread Group.

Внедрение внешнего Test Fragment из JMX-файла делается с помощью ноды Include Controller.

Подводные камни Include Controller:

Будьте внимательны с путями. В GUI всё работает отлично, но при запуске из командной строки (jmeter -n -t test_plan.jmx) пути до фрагментов резолвятся относительно рабочей директории, а не относительно основного JMX-файла. Если у вас тесты запускаются из CI/CD - убедитесь, что рабочая директория при запуске совпадает с той, из которой вы работаете в GUI. Иначе получите FileNotFoundException и будете долго искать причину.


Итоговая структура проекта

Было:

project/
└── src/test/jmeter/
    └── test_plan.jmx            # 50 000+ строк, вся логика внутри

Стало:

project/
└── src/test/jmeter/
    ├── test_plan.jmx                        # ~5 000 строк, только порядок выполнения
    ├── common/
    │   ├── sql_executor.groovy              # общий скрипт запуска SQL
    │   └── json_utils.groovy                # общие утилиты
    ├── domain_a/
    │   ├── domain_a_fragment.jmx            # Test Fragment для домена A
    │   ├── create_entity.sql
    │   ├── cleanup.sql
    │   ├── pre_processing.groovy
    │   └── response_validation.groovy
    ├── domain_b/
    │   ├── domain_b_fragment.jmx
    │   ├── prepare_data.sql
    │   ├── check_result.groovy
    │   └── post_processing.groovy
    └── domain_c/
        ├── domain_c_fragment.jmx
        ├── init.sql
        └── assertions.groovy
...

Каждая папка домена - самодостаточная единица. Открыл папку - сразу видишь и фрагмент теста, и все скрипты, которые к нему относятся.


Что в итоге?

Получилась архитектура, которая интуитивно понятна разработчикам, привыкшим к обычным проектам. Её легко ревьюить и поддерживать. А размер основного JMX-файла уменьшился в 10 раз.

Основная идея моей концепции: в JMX-файлах хранится только порядок выполнения тестов. Вся остальная логика и проверки выносятся в отдельные файлы с необходимыми расширениями, и работа с ними ведётся из IDE со всеми её плюсами.

Обсудить в форуме