- PVSM.RU - https://www.pvsm.ru -
Работая в Сбере, я столкнулся с тем, что общепринятым инструментом для функционального тестирования в моем трайбе был 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-тесты, чтобы добиться такого же результата.
Иногда для тестирования функциональности требуется подготовить таблицы в БД: добавить сущности, которые необходимы для тестов. Для этого в JMeter есть специальная нода - JDBC Request. И у неё есть некоторые минусы:
SQL-код этой ноды хранится в JMX-файле
У SQL нет подсказок и подсветки синтаксиса в интерфейсе JMeter
В идеале нам хотелось бы вынести эти SQL-скрипты в отдельные .sql файлы, которые можно было бы редактировать в IDE, пользуясь всеми её благами. Небольшой и быстрый гуглинг подсказывает нам, что в JMeter есть отличная встроенная функция __FileToString:
Однако у этой функции есть одна проблема - она не раскрывает автоматически переменные JMeter из 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 в отдельные файлы завершён.
Данный пункт достаточно очевиден, но хотелось бы его подсветить. Все ноды, которые начинаются с JSR223..., мы выносим в отдельные .groovy файлы. Все скрипты теперь не inline, а лежат в отдельных файлах. Работаем мы с ними напрямую в IDE, а не в интерфейсе JMeter.
Важный момент: при использовании внешних скриптов обязательно ставьте галочку «Cache compiled script if available» в настройках JSR223-элемента. Без неё JMeter будет перекомпилировать Groovy-скрипт при каждом вызове, что заметно просадит производительность.
Также, если JMeter-тестов несколько, то общие утилиты на Groovy можно вынести в отдельные файлы. Например, скрипт по запуску SQL-скриптов, указанный выше, лежит в папке common и доступен всем JMeter-тестам.
Оказывается, кусочки JMX-файла можно вынести в разные файлы, указав на них ссылки в главном JMX. Как это делается?
Конкретно в моём проекте тестирование каждого домена было вынесено в отдельный Thread Group. Достаточно было выделить всё, что вложено в каждый Thread Group, и нажать ПКМ → Save as Test Fragment, выбрав путь, где будет лежать логика этой Thread Group.
Внедрение внешнего Test Fragment из JMX-файла делается с помощью ноды 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 со всеми её плюсами.
Автор: makariyp
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/448318
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1017472/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1017472
Нажмите здесь для печати.