Powrót do strony głównej

Dekompozycja JMX JMeter: z 50k do 5k linii

Artykuł opisuje refaktoryzację monolitycznego pliku JMX JMeter na 50 000 linii. Wydzielenie SQL do .sql z podstawianiem zmiennych, Groovy do plików, modularność przez Test Fragment. Wynik: rozmiar 10 razy mniejszy, wygodny code review.

JMeter JMX monolit w moduły: refaktoryzacja na 10x
Advertisement 728x90

Dekompozycja testów JMeter: JMX z 50 000 linii do 5 000

Monolityczne pliki JMX JMeter z ponad 50 000 linii utrudniają code review, edycję i utrzymanie. Plik nie mieści się w pamięci, konflikty merge przy pracy równoległej są nieuniknione, diff w PR na 3000 linii XML jest nieczytelny. Skrypty JSR223 Groovy i SQL wewnątrz JMX pozbawione są podświetlania w IDE.

Dekompozycja zmniejszyła JMX do 5000 linii, logikę testów przeniesiono do oddzielnych plików. Testowanie endpointu skróciło się z 900 do 300 linii. Struktura projektu stała się modułowa, jak w zwykłym kodzie.

Przeniesienie skryptów SQL do oddzielnych plików

JDBC Request przechowuje SQL wewnątrz JMX bez podświetlania. Funkcja __FileToString odczytuje pliki .sql, ale nie rozpoznaje zmiennych JMeter ${varName} z vars.

Google AdInline article slot

Rozwiązanie — uniwersalny JSR223 Sampler w Groovy. Skrypt podstawia zmienne, wykonuje SELECT/DML, zapisuje wyniki w vars:

  • sql_result_count — liczba wierszy
  • sql_col_<N> — nazwy kolumn
  • sql_<wiersz>_<kolumna> — wartości
  • sql_result — pierwsza wartość
  • sql_affected_rows — zmienione wiersze dla DML
import groovy.sql.Sql
import java.util.regex.Pattern

// ============================================================
// JMeter JSR223 Sampler - Uniwersalny wykonawca SQL
// ============================================================
// Parametry (oddzielone spacją):
//   args[0] = JDBC URL      (jdbc:postgresql://host:5432/db)
//   args[1] = nazwa użytkownika bazy danych
//   args[2] = hasło bazy danych
//   args[3] = ścieżka do pliku .sql (może zawierać spacje)
//
// Wynik zapisywany w JMeter vars:
//   sql_result_count    - liczba wierszy
//   sql_col_count       - liczba kolumn
//   sql_col_<N>         - nazwa kolumny N (1-based)
//   sql_<wiersz>_<kolumna> - wartość (1-based, np. sql_1_1)
//   sql_result          - pierwsza wartość (skrót skalarny)
// ============================================================

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

// --- Odczytujemy SQL z pliku ---
def rawSql = new File(sqlFile).getText("UTF-8")

// --- Podstawiamy ${varName} -> vars.get("varName") ---
def resolved = rawSql.replaceAll(/\$\{(\w+)\}/) { fullMatch, varName ->
    def value = vars.get(varName)
    if (value == null) {
        log.warn("Zmienna '${varName}' nie znaleziona w JMeter vars, pozostawiamy bez zmian")
        return fullMatch
    }
    return value
}

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

// --- Łączymy się i wykonujemy ---
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 - oczekujemy 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 zwrócił 0 wierszy")
            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 zwrócił ${rows.size()} wiersz(e), ${colNames.size()} kolumn(y)")

    } else {
        // --- DML (INSERT/UPDATE/DELETE/MERGE itp.) ---
        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 wykonane, zmienione wiersze: ${affected}")
    }

} catch (Exception e) {
    log.error("Wykonanie SQL nie powiodło się: ${e.message}", e)
    throw e
} finally {
    sql.close()
}

Parametry w JSR223 Sampler: JDBC URL, nazwa użytkownika, hasło, ścieżka do .sql. Ścieżki względne działają w GUI i CLI przy zgodności katalogów roboczych.

Skrypty Groovy poza JMX

Elementy JSR223 przenosi się do plików .groovy. W ustawieniach włącza się Cache compiled script if available — bez tego kompilacja przy każdym wywołaniu obniża wydajność.

Google AdInline article slot

Wspólne narzędzia (sql_executor.groovy, json_utils.groovy) w folderze common, dostępne dla wszystkich testów.

Modularność przez Test Fragment i Include Controller

Thread Group według domen zapisuje się jako Test Fragment (PPM → Save as Test Fragment). Do głównego JMX dodaje się przez Include Controller.

Pułapki:

Google AdInline article slot
  • W CLI ścieżki rozwiązywane są od katalogu roboczego, nie od JMX
  • W CI/CD ustawiać cd do katalogu projektu

Struktura projektu po refaktoryzacji

Było:

project/
└── src/test/jmeter/
    └── test_plan.jmx            # 50 000+ linii

Stało się:

project/
└── src/test/jmeter/
    ├── test_plan.jmx                        # ~5 000 linii
    ├── common/
    │   ├── sql_executor.groovy
    │   └── json_utils.groovy
    ├── domain_a/
    │   ├── domain_a_fragment.jmx
    │   ├── create_entity.sql
    │   ├── cleanup.sql
    │   ├── pre_processing.groovy
    │   └── response_validation.groovy
    ├── domain_b/
    │   ├── domain_b_fragment.jmx
    │   ├── prepare_data.sql
    │   └── check_result.groovy
    └── domain_c/
        ├── domain_c_fragment.jmx
        ├── init.sql
        └── assertions.groovy

JMX zawiera tylko sekwencję wykonania. Logika w wyspecjalizowanych plikach z obsługą IDE.

Co jest ważne

  • Rozmiar JMX zmniejszył się 10-krotnie: 50 000 → 5000 linii
  • SQL i Groovy poza JMX z podświetlaniem i autouzupełnianiem
  • Test Fragment + Include Controller dla modularności
  • Uniwersalny skrypt SQL z podstawianiem ${varName}
  • Cache compiled script obowiązkowy dla Groovy

— Editorial Team

Advertisement 728x90

Czytaj dalej