Descomposición de Pruebas JMeter: De 50.000 a 5.000 Líneas en JMX
Los archivos JMX monolíticos en JMeter con más de 50.000 líneas convierten las revisiones de código, la edición y el mantenimiento en una pesadilla. El archivo no cabe en memoria, los conflictos de fusión durante el trabajo en paralelo son inevitables, y un diff XML de 3.000 líneas en una PR es ilegible. Los scripts JSR223 Groovy y SQL incrustados en JMX carecen de resaltado de sintaxis en el IDE.
La descomposición redujo el JMX a 5.000 líneas, trasladando la lógica de prueba a archivos separados. Probar un endpoint se redujo de 900 a 300 líneas. La estructura del proyecto se volvió modular, como el código regular.
Extracción de Scripts SQL en Archivos Separados
JDBC Request almacena SQL dentro de JMX sin resaltado. La función __FileToString lee archivos .sql pero no expande las variables de JMeter ${varName} de vars.
La solución es un Sampler JSR223 universal en Groovy. El script sustituye variables, ejecuta SELECT/DML y guarda resultados en vars:
sql_result_count— número de filassql_col_<N>— nombres de columnassql_<fila>_<col>— valoressql_result— primer valorsql_affected_rows— filas afectadas para DML
import groovy.sql.Sql
import java.util.regex.Pattern
// ============================================================
// JMeter JSR223 Sampler - Ejecutor SQL Universal
// ============================================================
// Parámetros (separados por espacios):
// args[0] = URL JDBC (jdbc:postgresql://host:5432/db)
// args[1] = usuario DB
// args[2] = contraseña DB
// args[3] = ruta al archivo .sql (puede incluir espacios)
//
// Los resultados se escriben en vars de JMeter:
// sql_result_count - conteo de filas
// sql_col_count - conteo de columnas
// sql_col_<N> - nombre de columna N (base 1)
// sql_<fila>_<col> - valor (base 1, ej., sql_1_1)
// sql_result - primer valor (atajo escalar)
// ============================================================
def jdbcUrl = args[0]
def dbUser = args[1]
def dbPass = args[2]
def sqlFile = args[3..args.length - 1].join(" ")
// --- Leer SQL del archivo ---
def rawSql = new File(sqlFile).getText("UTF-8")
// --- Sustituir ${varName} -> vars.get("varName") ---
def resolved = rawSql.replaceAll(/\$\{(\w+)\}/) { fullMatch, varName ->
def value = vars.get(varName)
if (value == null) {
log.warn("Variable '${varName}' no encontrada en vars de JMeter, dejando como está")
return fullMatch
}
return value
}
log.info("Ejecutando SQL:\n${resolved}")
// --- Conectar y ejecutar ---
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 - esperar 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 devolvió 0 filas")
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 devolvió ${rows.size()} fila(s), ${colNames.size()} col(s)")
} else {
// --- DML (INSERT/UPDATE/DELETE/MERGE, etc.) ---
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 ejecutado, filas afectadas: ${affected}")
}
} catch (Exception e) {
log.error("Ejecución SQL fallida: ${e.message}", e)
throw e
} finally {
sql.close()
}
Parámetros en JSR223 Sampler: URL JDBC, usuario, contraseña, ruta a .sql. Las rutas relativas funcionan en GUI y CLI si los directorios de trabajo coinciden.
Scripts Groovy Fuera de JMX
Los elementos JSR223 se trasladan a archivos .groovy. En configuraciones, habilita Cache compiled script if available — sin esto, la compilación en cada llamada reduce el rendimiento.
Utilidades comunes (sql_executor.groovy, json_utils.groovy) van en una carpeta common, accesible para todas las pruebas.
Modularidad mediante Fragmento de Prueba e Include Controller
Los Thread Groups por dominio se guardan como Fragmentos de Prueba (clic derecho → Save as Test Fragment). Se añaden al JMX principal mediante Include Controller.
Trampas:
- En CLI, las rutas se resuelven desde el directorio de trabajo, no desde el JMX
- En CI/CD, establece
cdal directorio del proyecto
Estructura del Proyecto Después de la Refactorización
Antes:
proyecto/
└── src/test/jmeter/
└── plan_de_prueba.jmx # 50.000+ líneas
Después:
proyecto/
└── src/test/jmeter/
├── plan_de_prueba.jmx # ~5.000 líneas
├── common/
│ ├── sql_executor.groovy
│ └── json_utils.groovy
├── dominio_a/
│ ├── fragmento_dominio_a.jmx
│ ├── crear_entidad.sql
│ ├── limpieza.sql
│ ├── pre_procesamiento.groovy
│ └── validacion_respuesta.groovy
├── dominio_b/
│ ├── fragmento_dominio_b.jmx
│ ├── preparar_datos.sql
│ └── verificar_resultado.groovy
└── dominio_c/
├── fragmento_dominio_c.jmx
├── inicializacion.sql
└── aserciones.groovy
JMX contiene solo la secuencia de ejecución. La lógica está en archivos especializados con soporte de IDE.
Conclusiones Clave
- Tamaño JMX reducido 10x: 50.000 → 5.000 líneas
- SQL y Groovy fuera de JMX con resaltado y autocompletado
- Fragmento de Prueba + Include Controller para modularidad
- Script SQL universal con sustitución de
${varName} - Cache compiled script es esencial para Groovy
— Editorial Team
Aún no hay comentarios.