Gatling es una poderosa herramienta de load testing (como se discute en K6: Modern Load Testing with JavaScript for DevOps Teams) de alto rendimiento diseñada para probar aplicaciones web, APIs y microservicios. Construido sobre Akka y Netty, Gatling (como se discute en Load Testing with JMeter: Complete Guide) sobresale simulando miles de usuarios concurrentes con consumo mínimo de recursos mientras proporciona métricas detalladas y accionables a través de hermosos reportes HTML.
¿Por Qué Gatling para Pruebas de Rendimiento?
Gatling se destaca en el panorama de pruebas de rendimiento a través de su combinación única de DSL amigable para desarrolladores, rendimiento excepcional y características enterprise-grade:
- Scala DSL: Scripting expresivo y type-safe con soporte IDE
- Alto rendimiento: Arquitectura async non-blocking maneja concurrencia masiva
- Simulaciones realistas: Modelado avanzado de usuarios con think times y pacing
- Protocolos ricos: Soporte HTTP, WebSocket, SSE, JMS out of the box
- Reportes hermosos: Reportes HTML detallados con gráficos y métricas
- Listo para CI/CD: Ejecución por línea de comandos e integración Maven/Gradle
Gatling vs Otras Herramientas de Load Testing
Característica | Gatling | JMeter | K6 | Locust |
---|---|---|---|---|
Scripting | Scala DSL | GUI/XML | JavaScript | Python |
Rendimiento | Excelente | Medio | Excelente | Medio |
Uso de Recursos | Bajo | Alto | Bajo | Medio |
Soporte de Protocolos | HTTP, WS, JMS | Extenso | HTTP/2, WS, gRPC | HTTP |
Reporting | Excelente | Básico | Bueno | Básico |
Recorder | Sí | Sí | No | No |
Integración CI/CD | Nativa | Plugins | Nativa | Manual |
Comenzando con Gatling
Instalación con Maven
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gatling-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<gatling.version>3.10.3</gatling.version>
<gatling-maven-plugin.version>4.7.0</gatling-maven-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling-maven-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
Instalación con Gradle
plugins {
id 'scala'
id 'io.gatling.gradle' version '3.10.3.1'
}
repositories {
mavenCentral()
}
dependencies {
gatling 'io.gatling.highcharts:gatling-charts-highcharts:3.10.3'
}
gatling {
simulations = {
include '**/*Simulation.scala'
}
}
Primera Simulación Gatling
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class BasicSimulation extends Simulation {
// Configuración del protocolo HTTP
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json")
.userAgentHeader("Gatling Performance Test")
// Definición del escenario
val scn = scenario("Basic Load Test")
.exec(
http("Get Users")
.get("/users")
.check(status.is(200))
)
.pause(1)
.exec(
http("Get User Details")
.get("/users/1")
.check(status.is(200))
.check(jsonPath("$.name").exists)
)
// Inyección de carga
setUp(
scn.inject(
atOnceUsers(10), // 10 usuarios inmediatamente
rampUsers(50) during (30.seconds) // 50 usuarios en 30 segundos
)
).protocols(httpProtocol)
}
Ejecutar la simulación:
mvn gatling:test
# o
./gradlew gatlingRun
Perfiles de Inyección de Carga
Modelos de Carga Abiertos vs Cerrados
// Modelo Abierto: Nuevos usuarios llegan independientemente del rendimiento del sistema
setUp(
scn.inject(
nothingFor(4.seconds), // Esperar 4 segundos
atOnceUsers(10), // Inyectar 10 usuarios a la vez
rampUsers(50) during (30.seconds), // Subir a 50 usuarios
constantUsersPerSec(20) during (1.minute), // Tasa constante
rampUsersPerSec(10) to 50 during (2.minutes) // Rampa de tasa
)
)
// Modelo Cerrado: Número fijo de usuarios concurrentes
setUp(
scn.inject(
constantConcurrentUsers(10) during (30.seconds),
rampConcurrentUsers(10) to 50 during (1.minute)
)
)
Patrones de Inyección Avanzados
setUp(
// Patrón de stress test
scn.inject(
rampUsers(10) during (1.minute), // Calentamiento
constantUsersPerSec(100) during (5.minutes), // Carga sostenida
rampUsersPerSec(100) to 500 during (2.minutes), // Rampa al pico
constantUsersPerSec(500) during (5.minutes), // Carga pico
rampUsersPerSec(500) to 100 during (2.minutes) // Enfriamiento
),
// Spike test
scn.inject(
constantUsersPerSec(50) during (2.minutes),
atOnceUsers(500), // Spike repentino
constantUsersPerSec(50) during (2.minutes)
)
).protocols(httpProtocol)
Throttling
setUp(
scn.inject(rampUsers(1000) during (1.minute))
).protocols(httpProtocol)
.throttle(
reachRps(100) in (10.seconds), // Alcanzar 100 RPS
holdFor(1.minute), // Mantener
jumpToRps(50), // Bajar a 50
holdFor(2.minutes)
)
Configuración del Protocolo HTTP
Configuración Completa del Protocolo HTTP
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json, text/html, */*")
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("en-US,en;q=0.5")
.userAgentHeader("Gatling/3.10")
.shareConnections // Connection pooling
.disableWarmUp // Saltar warmup requests
// Headers
.header("X-API-Version", "v1")
// Autenticación
.authorizationHeader("Bearer ${token}")
// Proxy
.proxy(Proxy("proxy.example.com", 8080)
.credentials("username", "password"))
// Timeouts
.requestTimeout(30.seconds)
.readTimeout(30.seconds)
// Keep-alive
.connectionHeader("keep-alive")
// Manejo automático de redirecciones
.disableFollowRedirect
Construyendo Escenarios Complejos
Composición de Escenarios
// Componentes de escenario reutilizables
val browse = exec(
http("Homepage")
.get("/")
.check(status.is(200))
).pause(2, 5) // Pausa aleatoria entre 2-5 segundos
val search = exec(
http("Search Products")
.get("/search")
.queryParam("q", "laptop")
.check(status.is(200))
.check(jsonPath("$.results[*].id").findAll.saveAs("productIds"))
).pause(1, 3)
val viewProduct = exec(
http("View Product")
.get("/products/${productId}")
.check(status.is(200))
).pause(3, 7)
// Componer en escenario completo
val scn = scenario("E-commerce User Journey")
.exec(browse)
.exec(search)
.exec(session => {
val productIds = session("productIds").as[Vector[String]]
val randomId = productIds(scala.util.Random.nextInt(productIds.length))
session.set("productId", randomId)
})
.exec(viewProduct)
Ejecución Condicional
val scn = scenario("Flujo Condicional")
.exec(
http("Get User")
.get("/users/me")
.check(status.is(200))
.check(jsonPath("$.role").saveAs("userRole"))
)
.doIf(session => session("userRole").as[String] == "admin") {
exec(
http("Admin Dashboard")
.get("/admin/dashboard")
.check(status.is(200))
)
}
.doIfOrElse(session => session("userRole").as[String] == "premium") {
exec(http("Premium Features").get("/premium"))
} {
exec(http("Standard Features").get("/standard"))
}
Loops e Iteraciones
val scn = scenario("Ejemplo de Loop")
// Repetir número fijo de veces
.repeat(5) {
exec(http("Request ${counter}").get("/api/data"))
.pause(1)
}
// Repetir mientras la condición es verdadera
.asLongAs(session => session("continue").as[Boolean]) {
exec(http("Poll").get("/api/status"))
.pause(5)
}
// Repetir por duración
.during(30.seconds) {
exec(http("Sustained Load").get("/api/endpoint"))
.pause(1)
}
// Iterar sobre colección
.foreach(List("product1", "product2", "product3"), "productId") {
exec(
http("Get Product ${productId}")
.get("/products/${productId}")
)
}
Construcción de Solicitudes y Validación
Solicitudes POST con Body
val scn = scenario("Operaciones CRUD de API")
// JSON body desde string
.exec(
http("Create User")
.post("/users")
.body(StringBody("""{"name": "John", "email": "john@example.com"}"""))
.asJson
.check(status.is(201))
.check(jsonPath("$.id").saveAs("userId"))
)
// JSON body desde template
.exec(
http("Create Order")
.post("/orders")
.body(ElFileBody("templates/order.json"))
.asJson
)
// Form data
.exec(
http("Submit Form")
.post("/submit")
.formParam("field1", "value1")
.formParam("field2", "${dynamicValue}")
)
Checks Avanzados
val scn = scenario("Validación de Respuesta")
.exec(
http("Validación Compleja")
.get("/api/products")
.check(status.is(200))
// JSON path checks
.check(jsonPath("$.products[*]").count.gte(10))
.check(jsonPath("$.products[0].name").exists)
.check(jsonPath("$.products[*].price").findAll.saveAs("prices"))
// Response time check
.check(responseTimeInMillis.lte(500))
// Header checks
.check(header("Content-Type").is("application/json"))
// Body substring check
.check(substring("success").exists)
// Expresión regular
.check(regex("""id":\s*(\d+)""").findAll.saveAs("ids"))
// Verificación MD5/SHA hash
.check(md5.is("expected-hash"))
)
Gestión de Sesión
val scn = scenario("Variables de Sesión")
.exec { session =>
println(s"Session ID: ${session.userId}")
session.set("customVar", "value")
}
.exec(
http("Login")
.post("/login")
.body(StringBody("""{"username": "${username}", "password": "${password}"}"""))
.check(jsonPath("$.token").saveAs("authToken"))
)
.exec(
http("Authenticated Request")
.get("/protected")
.header("Authorization", "Bearer ${authToken}")
)
// Acceder datos de sesión
.exec { session =>
val token = session("authToken").as[String]
println(s"Using token: $token")
session
}
Feeders para Tests Data-Driven
CSV Feeder
// users.csv:
// username,password,role
// user1,pass1,admin
// user2,pass2,user
val csvFeeder = csv("users.csv").random // Selección aleatoria
val csvFeederCircular = csv("users.csv").circular // Round-robin
val csvFeederQueue = csv("users.csv").queue // Secuencial, una sola vez
val scn = scenario("Login Data-Driven")
.feed(csvFeeder)
.exec(
http("Login as ${username}")
.post("/login")
.body(StringBody("""{"username": "${username}", "password": "${password}"}"""))
.check(status.is(200))
)
Feeders JSON y Personalizados
// JSON feeder
val jsonFeeder = jsonFile("products.json").random
// Array feeder
val customFeeder = Array(
Map("product" -> "Laptop", "price" -> "999"),
Map("product" -> "Phone", "price" -> "599")
).random
// Generated feeder
val idFeeder = Iterator.continually(
Map("userId" -> scala.util.Random.nextInt(1000))
)
val scn = scenario("Custom Feeder")
.feed(idFeeder)
.exec(
http("Get User ${userId}")
.get("/users/${userId}")
)
Escenarios de Prueba del Mundo Real
Flujo Completo de Usuario E-commerce
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class EcommerceSimulation extends Simulation {
val httpProtocol = http
.baseUrl("https://ecommerce.example.com")
.acceptHeader("application/json")
val csvFeeder = csv("users.csv").circular
// Escenario: Navegar y Comprar
val browse = scenario("Browse Products")
.feed(csvFeeder)
.exec(
http("Homepage")
.get("/")
.check(status.is(200))
)
.pause(2, 4)
.exec(
http("Category Page")
.get("/category/electronics")
.check(status.is(200))
.check(jsonPath("$.products[*].id").findAll.saveAs("productIds"))
)
.pause(3, 6)
val purchase = scenario("Complete Purchase")
.feed(csvFeeder)
.exec(browse)
.exec { session =>
val productIds = session("productIds").as[Vector[String]]
session.set("selectedProduct", productIds.head)
}
.exec(
http("Product Details")
.get("/products/${selectedProduct}")
.check(status.is(200))
)
.pause(5, 10)
.exec(
http("Add to Cart")
.post("/cart")
.body(StringBody("""{"productId": "${selectedProduct}", "quantity": 1}"""))
.asJson
.check(status.is(200))
)
.pause(2, 5)
.exec(
http("Checkout")
.post("/checkout")
.body(ElFileBody("checkout.json"))
.asJson
.check(status.is(201))
)
// Simulación de carga
setUp(
browse.inject(
rampUsers(100) during (2.minutes)
),
purchase.inject(
rampUsers(50) during (3.minutes)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile3.lt(1500),
global.successfulRequests.percent.gt(95)
)
}
Integración CI/CD
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git 'https://github.com/your/repo.git'
}
}
stage('Run Gatling Tests') {
steps {
sh 'mvn gatling:test'
}
}
stage('Publish Reports') {
steps {
gatlingArchive()
}
}
}
post {
always {
publishHTML([
reportDir: 'target/gatling',
reportFiles: 'index.html',
reportName: 'Gatling Report'
])
}
}
}
GitHub Actions
name: Performance Tests
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
gatling:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run Gatling Tests
run: mvn gatling:test
- name: Upload Gatling Results
uses: actions/upload-artifact@v3
if: always()
with:
name: gatling-results
path: target/gatling/
Assertions y SLAs
setUp(
scn.inject(rampUsers(100) during (1.minute))
).protocols(httpProtocol)
.assertions(
// Assertions globales
global.responseTime.max.lt(5000),
global.responseTime.mean.lt(1000),
global.responseTime.percentile3.lt(1500), // Percentil 95
global.responseTime.percentile4.lt(2000), // Percentil 99
// Tasa de éxito
global.successfulRequests.percent.gt(99),
global.failedRequests.count.lt(10),
// Assertions específicas de solicitud
forAll.responseTime.max.lt(3000),
details("Homepage").responseTime.max.lt(500),
details("API Call").successfulRequests.percent.is(100)
)
Mejores Prácticas
Organización de Simulaciones
// BaseSimulation.scala - Configuración común
abstract class BaseSimulation extends Simulation {
val httpProtocol = http
.baseUrl(System.getProperty("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
.userAgentHeader("Gatling")
val defaultPause = 1.second
}
// Simulaciones específicas extienden base
class UserSimulation extends BaseSimulation {
// Código específico de simulación
}
Think Time y Pacing
val scn = scenario("Usuario Realista")
.exec(http("Action 1").get("/page1"))
.pause(2, 5) // Aleatorio entre 2-5 segundos
.exec(http("Action 2").get("/page2"))
.pace(10.seconds) // Mantener 10s entre iteraciones
.exec(http("Action 3").get("/page3"))
.rendezVous(100) // Esperar hasta que 100 usuarios alcancen este punto
Conclusión
Gatling combina alto rendimiento con productividad de desarrollador, convirtiéndolo en una excelente elección para pruebas de rendimiento modernas. Su Scala DSL proporciona type safety y expresividad, mientras su arquitectura async ofrece capacidades excepcionales de generación de carga.
Fortalezas clave:
- Arquitectura async de alto rendimiento
- Scala DSL expresivo con soporte IDE
- Reportes HTML hermosos y detallados
- Excelente soporte de protocolos
- Fuerte integración CI/CD
- Características enterprise-ready
Ya sea probando REST APIs, microservicios o aplicaciones web completas, Gatling proporciona el rendimiento, características y reporting necesarios para load testing (como se discute en Artillery Performance Testing: Modern Load Testing with YAML Scenarios) comprehensivo en entornos DevOps modernos.