TL;DR: Gatling es una herramienta de pruebas de carga code-first con DSL Scala que maneja 10x más usuarios concurrentes que JMeter con el mismo hardware. Escribe escenarios como código Scala, usa feeders para datos de prueba y configura aserciones para umbrales CI/CD.

Gatling es utilizado por más de 100.000 desarrolladores en todo el mundo y está en el ThoughtWorks Technology Radar como la herramienta recomendada para pruebas de rendimiento code-first. Construido sobre una arquitectura async Netty/Akka, Gatling maneja 10x más usuarios concurrentes que JMeter en hardware equivalente. Según la encuesta State of Performance Testing 2024, la adopción de Gatling creció un 34% interanual. El DSL Scala permite escribir escenarios expresivos, testing orientado a datos mediante feeders y generación automática de informes HTML con desglose por percentiles. A diferencia de JMeter, las simulaciones de Gatling son clases Scala — puedes versionarlas y revisarlas en el mismo PR que el código de la aplicación. Este tutorial cubre el kit completo de Gatling: fundamentos del DSL Scala, configuración HTTP, feeders, aserciones, integración CI/CD e interpretación de informes HTML.

¿Qué es Gatling?

Gatling es un framework open-source para pruebas de carga construido sobre Scala, Akka y Netty. Usa una arquitectura asíncrona y no bloqueante que simula eficientemente cargas masivas de usuarios concurrentes.

Por qué Gatling:

  • Alto rendimiento — modelo asíncrono maneja 10,000+ usuarios por máquina
  • Código como tests — Scala DSL se integra con control de versiones y CI/CD
  • Reportes hermosos — reportes HTML detallados con gráficos y percentiles
  • Amigable para desarrolladores — soporte de IDE, debugging, reutilización de código
  • Eficiente en recursos — menor CPU/memoria que herramientas basadas en threads

Instalación

Prerrequisitos

# Se requiere Java 11+
java -version

# Descargar Gatling
# Opción 1: Descargar desde https://gatling.io/open-source/
# Opción 2: Dependencia Maven/Gradle

Maven Setup

<dependencies>
    <dependency>
        <groupId>io.gatling.highcharts</groupId>
        <artifactId>gatling-charts-highcharts</artifactId>
        <version>3.10.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>io.gatling</groupId>
            <artifactId>gatling-maven-plugin</artifactId>
            <version>4.7.0</version>
        </plugin>
    </plugins>
</build>

Estructura del Proyecto

project/
├── src/
│   └── test/
│       ├── scala/
│       │   └── simulations/
│       │       └── BasicSimulation.scala
│       └── resources/
│           ├── gatling.conf
│           ├── data/
│           │   └── users.csv
│           └── bodies/
│               └── request.json
├── pom.xml
└── target/
    └── gatling/
        └── results/

Primera Simulación

Estructura Básica

package simulations

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BasicSimulation extends Simulation {

  // Configuración HTTP
  val httpProtocol = http
    .baseUrl("https://api.example.com")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")

  // Definición del Escenario
  val scn = scenario("Basic API Test")
    .exec(
      http("Get Users")
        .get("/users")
        .check(status.is(200))
    )
    .pause(1.second)
    .exec(
      http("Get User Details")
        .get("/users/1")
        .check(status.is(200))
        .check(jsonPath("$.name").exists)
    )

  // Perfil de Carga
  setUp(
    scn.inject(atOnceUsers(10))
  ).protocols(httpProtocol)
}

Ejecutando Tests

# Ejecutar todas las simulaciones
mvn gatling:test

# Ejecutar simulación específica
mvn gatling:test -Dgatling.simulationClass=simulations.BasicSimulation

# Con Gatling bundle
./bin/gatling.sh

Requests HTTP

Tipos de Request

class HttpExamplesSimulation extends Simulation {

  val httpProtocol = http.baseUrl("https://api.example.com")

  val scn = scenario("HTTP Examples")
    // Request GET
    .exec(
      http("Get Request")
        .get("/users")
        .queryParam("page", "1")
        .queryParam("limit", "10")
    )
    // POST con cuerpo JSON
    .exec(
      http("Create User")
        .post("/users")
        .body(StringBody("""{"name":"John","email":"john@example.com"}"""))
        .asJson
    )
    // POST con cuerpo desde archivo
    .exec(
      http("Create from File")
        .post("/users")
        .body(RawFileBody("bodies/user.json"))
        .asJson
    )
    // Request PUT
    .exec(
      http("Update User")
        .put("/users/1")
        .body(StringBody("""{"name":"Updated Name"}"""))
        .asJson
    )
    // Request DELETE
    .exec(
      http("Delete User")
        .delete("/users/1")
    )

  setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}

Headers y Autenticación

val httpProtocol = http
  .baseUrl("https://api.example.com")
  .acceptHeader("application/json")
  .contentTypeHeader("application/json")
  .authorizationHeader("Bearer ${token}")
  .userAgentHeader("Gatling/3.10")

// O por request
.exec(
  http("Authenticated Request")
    .get("/protected")
    .header("Authorization", "Bearer ${token}")
    .header("X-Custom-Header", "value")
)

Sesión y Variables

Guardando Datos de Respuesta

val scn = scenario("Session Example")
  // Guardar valor de respuesta
  .exec(
    http("Login")
      .post("/auth/login")
      .body(StringBody("""{"email":"user@example.com","password":"pass123"}"""))
      .asJson
      .check(jsonPath("$.token").saveAs("authToken"))
      .check(jsonPath("$.user.id").saveAs("userId"))
  )
  // Usar valores guardados
  .exec(
    http("Get Profile")
      .get("/users/${userId}")
      .header("Authorization", "Bearer ${authToken}")
  )
  // Debug: imprimir sesión
  .exec { session =>
    println(s"Token: ${session("authToken").as[String]}")
    println(s"User ID: ${session("userId").as[String]}")
    session
  }

Funciones de Sesión

.exec { session =>
  // Modificar sesión
  session.set("customVar", "value")
}

.exec { session =>
  // Lógica condicional
  val userId = session("userId").as[String]
  if (userId.toInt > 100) {
    session.set("userType", "premium")
  } else {
    session.set("userType", "standard")
  }
}

Feeders (Datos de Test)

CSV Feeder

# data/users.csv
username,password,role
john@example.com,pass123,admin
jane@example.com,pass456,user
bob@example.com,pass789,user
val csvFeeder = csv("data/users.csv").random

val scn = scenario("Login Test")
  .feed(csvFeeder)
  .exec(
    http("Login")
      .post("/auth/login")
      .body(StringBody("""{"email":"${username}","password":"${password}"}"""))
      .asJson
  )

Estrategias de Feeder

// Secuencial - cada usuario obtiene la siguiente fila
val seqFeeder = csv("data/users.csv").queue

// Circular - vuelve al inicio
val circularFeeder = csv("data/users.csv").circular

// Aleatorio - fila aleatoria cada vez
val randomFeeder = csv("data/users.csv").random

// Mezclado - aleatorio pero cada fila se usa una vez
val shuffleFeeder = csv("data/users.csv").shuffle

// JSON feeder
val jsonFeeder = jsonFile("data/users.json").circular

// Feeder personalizado
val customFeeder = Iterator.continually(Map(
  "email" -> s"user${scala.util.Random.nextInt(1000)}@example.com",
  "timestamp" -> System.currentTimeMillis()
))

Checks y Assertions

Checks de Respuesta

.exec(
  http("Get Users")
    .get("/users")
    // Check de estado
    .check(status.is(200))
    // Check de tiempo de respuesta
    .check(responseTimeInMillis.lt(2000))
    // Check de header
    .check(header("Content-Type").is("application/json"))
    // Checks JSON
    .check(jsonPath("$").exists)
    .check(jsonPath("$.data").ofType[Seq[Any]])
    .check(jsonPath("$.data[*].id").findAll.saveAs("userIds"))
    .check(jsonPath("$.meta.total").ofType[Int].gt(0))
    // Check de cuerpo como string
    .check(bodyString.exists)
    .check(substring("success").exists)
)

Assertions Globales

setUp(
  scn.inject(rampUsers(100).during(60.seconds))
).protocols(httpProtocol)
  .assertions(
    // Assertions de tiempo de respuesta
    global.responseTime.max.lt(5000),
    global.responseTime.percentile(95).lt(2000),
    global.responseTime.mean.lt(1000),
    // Tasa de éxito
    global.successfulRequests.percent.gt(99),
    // Requests por segundo
    global.requestsPerSec.gt(100),
    // Request específico
    details("Login").responseTime.max.lt(3000),
    details("Login").failedRequests.percent.lt(1)
  )

Perfiles de Carga

Patrones de Inyección

setUp(
  // Usuarios fijos de una vez
  scn.inject(atOnceUsers(100)),

  // Rampa a lo largo del tiempo
  scn.inject(rampUsers(100).during(60.seconds)),

  // Tasa constante
  scn.inject(constantUsersPerSec(10).during(5.minutes)),

  // Tasa con rampa
  scn.inject(
    rampUsersPerSec(1).to(100).during(2.minutes)
  ),

  // Carga escalonada
  scn.inject(
    incrementUsersPerSec(10)
      .times(5)
      .eachLevelLasting(30.seconds)
      .separatedByRampsLasting(10.seconds)
      .startingFrom(10)
  ),

  // Perfil complejo
  scn.inject(
    nothingFor(5.seconds),
    atOnceUsers(10),
    rampUsers(50).during(30.seconds),
    constantUsersPerSec(20).during(2.minutes),
    rampUsersPerSec(20).to(50).during(1.minute)
  )
)

Múltiples Escenarios

val loginScn = scenario("Login Flow")
  .exec(/* login steps */)

val browseScn = scenario("Browse Products")
  .exec(/* browse steps */)

val checkoutScn = scenario("Checkout")
  .exec(/* checkout steps */)

setUp(
  loginScn.inject(rampUsers(100).during(1.minute)),
  browseScn.inject(rampUsers(200).during(1.minute)),
  checkoutScn.inject(rampUsers(50).during(1.minute))
).protocols(httpProtocol)

Control de Flujo del Escenario

Pausas

val scn = scenario("With Pauses")
  .exec(http("Request 1").get("/api/1"))
  .pause(1.second)                     // Pausa fija
  .exec(http("Request 2").get("/api/2"))
  .pause(1.second, 3.seconds)          // Aleatoria entre 1-3s
  .exec(http("Request 3").get("/api/3"))
  .pause(normalPausesWithStdDevDuration(2.seconds, 500.millis)) // Distribución normal

Bucles y Condiciones

val scn = scenario("Flow Control")
  // Repetir cantidad fija
  .repeat(5) {
    exec(http("Repeated").get("/api/data"))
  }
  // Repetir con contador
  .repeat(3, "counter") {
    exec(http("Item ${counter}").get("/api/items/${counter}"))
  }
  // Bucle durante tiempo
  .during(30.seconds) {
    exec(http("Looped").get("/api/status"))
      .pause(1.second)
  }
  // Ejecución condicional
  .doIf("${userType.equals('premium')}") {
    exec(http("Premium Feature").get("/api/premium"))
  }
  // Switch aleatorio
  .randomSwitch(
    60.0 -> exec(http("Path A").get("/api/a")),
    40.0 -> exec(http("Path B").get("/api/b"))
  )

Manejo de Errores

val scn = scenario("Error Handling")
  .exec(
    http("Might Fail")
      .get("/api/unreliable")
      .check(status.is(200))
  )
  .exitHereIfFailed  // Detener usuario si el anterior falló
  .exec(http("After Success").get("/api/next"))

// Estilo try-catch
.tryMax(3) {
  exec(http("Retry Request").get("/api/flaky"))
}

// Salir de bloque en falla
.exitBlockOnFail {
  exec(http("Critical").get("/api/critical"))
  exec(http("Dependent").get("/api/dependent"))
}

Reportes

Reporte HTML

Gatling genera reportes HTML detallados automáticamente:

target/gatling/basicsimulation-20260128120000/
├── index.html              # Reporte principal
├── js/
├── style/
└── simulation.log

Secciones del reporte:

  • Global Information — total de requests, tasas de éxito/falla
  • Response Time Distribution — histograma de tiempos de respuesta
  • Response Time Percentiles — percentiles 50th, 75th, 95th, 99th en el tiempo
  • Requests per Second — gráfico de throughput
  • Responses per Second — tasa de respuesta del servidor
  • Active Users — usuarios concurrentes durante el test

Integración CI/CD

GitHub Actions

name: Load Tests

on:
  schedule:
    - cron: '0 2 * * *'  # Diario a las 2 AM
  workflow_dispatch:

jobs:
  gatling:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Run Gatling tests
        run: mvn gatling:test -Dgatling.simulationClass=simulations.LoadTest

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: gatling-report
          path: target/gatling/*/

      - name: Fail on assertion errors
        run: |
          if grep -q "KO" target/gatling/*/simulation.log; then
            echo "Performance assertions failed!"
            exit 1
          fi

Gatling con Asistencia de IA

Las herramientas de IA pueden ayudar a escribir y optimizar simulaciones de Gatling.

Lo que la IA hace bien:

  • Generar escenarios desde documentación de API
  • Crear datos realistas para feeders
  • Sugerir perfiles de carga apropiados
  • Convertir otros formatos a Gatling DSL

Lo que aún necesita humanos:

  • Definir patrones de comportamiento de usuario realistas
  • Establecer umbrales de rendimiento significativos
  • Analizar resultados en contexto de negocio
  • Decisiones de planificación de capacidad

“Gatling cambió nuestra cultura de pruebas de rendimiento. Cuando las pruebas de carga viven como código en el mismo repositorio que la aplicación, los desarrolladores realmente las leen, las mejoran y detectan regresiones durante la revisión de código — no después del despliegue.” — Yuri Kan, Senior QA Lead

FAQ

¿Qué es Gatling?

Gatling es una herramienta de pruebas de carga open-source basada en Scala diseñada para alto rendimiento y workflows amigables para desarrolladores. Usa una arquitectura asíncrona y no bloqueante construida sobre Akka y Netty que simula eficientemente miles de usuarios concurrentes con bajo consumo de recursos. Los tests se escriben como código usando un DSL Scala legible.

¿Gatling es gratis?

Sí, Gatling Open Source es completamente gratuito bajo la licencia Apache 2.0. Incluye el motor de testing completo, Scala DSL y reportería HTML. Gatling Enterprise (anteriormente Gatling FrontLine) es un producto pago que añade testing distribuido, monitoreo en tiempo real, analíticas avanzadas y funciones de colaboración de equipo para organizaciones que necesitan escala adicional y capacidades de gestión.

¿Gatling vs JMeter — cuál es mejor?

Gatling sobresale en escenarios de alta concurrencia con menor uso de recursos debido a su arquitectura asíncrona — una instancia de Gatling puede simular tantos usuarios como 3-4 instancias de JMeter. Los scripts de Gatling son código, haciéndolos mantenibles y amigables con CI/CD. JMeter tiene una GUI que es más fácil para principiantes y un ecosistema más grande de plugins. Elige Gatling para testing de rendimiento liderado por desarrolladores; elige JMeter para equipos que prefieren creación visual de tests.

¿Necesito saber Scala para Gatling?

Conocimientos básicos de Scala ayudan pero no son requeridos para ser productivo. El DSL de Gatling está diseñado para ser legible y la mayoría de escenarios usan cadenas de métodos simples como .get(), .check(), .saveAs(). Puedes escribir pruebas de carga efectivas en pocas horas de empezar. Para escenarios complejos con lógica personalizada, el conocimiento de Scala se vuelve más útil para manipulación de sesiones y flujos condicionales.

Recursos Oficiales

Ver También