The most surprising thing about Gatling with Kotlin is how much of the boilerplate you can eliminate, letting you focus on the what of your test, not the how of writing it.

Imagine you’re testing an API that lists users. Here’s what a Gatling scenario might look like in Kotlin:

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

class UserApiSimulation extends Simulation {

  val httpProtocol = http
    .baseUrl("http://localhost:8080") // The base URL for all your requests
    .acceptHeader("application/json") // Always expect JSON back

  val scn = scenario("User List Scenario") // Give your scenario a descriptive name
    .exec(
      http("List Users") // The name of this specific HTTP request in reports
        .get("/users") // The HTTP method and path
        .check(status.is(200)) // Assert that the response status is 200 OK
        .check(jsonPath("$[*]").ofType[List[_]].saveAs("userList")) // Extract the user list and save it
    )
    .pause(1.seconds, 5.seconds) // Wait between 1 and 5 seconds before the next action
    .exec(
      http("Get Specific User")
        .get("/users/${userId}") // Use a variable from the previous step
        .check(status.is(200))
    )
    .repeat(5) { // Repeat the following block 5 times
      exec(
        http("Create User")
          .post("/users")
          .body(StringBody("""{"name": "Test User", "email": "test@example.com"}""")) // Send a JSON payload
          .check(status.is(201)) // Expect a 201 Created status
          .check(jsonPath("$.id").saveAs("userId")) // Save the ID of the newly created user
      )
      .pause(2.seconds) // Wait 2 seconds after creating a user
    }

  setUp(
    scn.inject(
      rampUsers(10).during(10.seconds), // Gradually ramp up to 10 users over 10 seconds
      constantUsers(50).during(30.seconds) // Maintain 50 users for 30 seconds
    )
  ).protocols(httpProtocol) // Apply the HTTP protocol to this setup
}

This code defines a simulation that first requests a list of users, then creates users one by one, and for each created user, it immediately requests that specific user’s details. The repeat block, pause, and rampUsers/constantUsers are all ways to control the flow and volume of your simulated traffic.

What problem does Gatling solve? It allows you to simulate a large number of concurrent users interacting with your application. This is crucial for understanding performance bottlenecks, identifying breaking points, and ensuring your system can handle expected (and unexpected) load. Gatling’s core components are:

  • Protocols: Define how your simulation communicates. HttpProtocolBuilder is common, specifying baseUrl, default headers, and other HTTP-specific settings.
  • Scenarios: Describe the sequence of actions a virtual user will perform. This includes making requests, executing code, pausing, repeating, and conditional logic.
  • Injectors: Define how users are introduced into the simulation over time. You can have immediate injections, gradual ramps, or sustained constant loads.
  • Simulations: The top-level class that ties everything together, defining the scenarios to run, how many users to inject, and which protocols to use.

The Kotlin DSL shines here by using idiomatic Kotlin features. For example, scenario("User List Scenario") is more readable than a Java equivalent. The exec block allows you to chain operations, and the check DSL provides a fluent way to validate responses. jsonPath("$[*]").ofType[List[_]].saveAs("userList") is a prime example: it targets a JSON array, asserts it’s a list, and saves its contents into a Gatling session variable named userList for later use.

When you run this simulation, Gatling spins up a JVM process. Each virtual user is essentially a lightweight thread managed by Gatling’s Akka Actor system. These actors execute the scenario steps concurrently. The setUp block orchestrates the creation and management of these user actors according to the defined injection profiles. The protocols are then applied to these user actors to manage their network communication.

The way Gatling handles dynamic data and state across requests is through the Session object. Every exec block receives the current Session and returns a potentially modified Session. When you saveAs("userId"), you’re adding a key-value pair to this Session. The next exec block can then access this value using ${userId}. This session-based approach is how state is maintained within a single virtual user’s journey, and it’s a powerful mechanism for creating realistic, stateful tests.

You might not realize that the check DSL doesn’t just validate; it also transforms data. When you use jsonPath("$.id").saveAs("userId"), Gatling doesn’t just check if an ID exists; it parses the JSON, extracts the value associated with the id key, and then stores that extracted value. This extraction is the crucial part that enables chaining dependent requests, like fetching a user by the ID of a user you just created.

The next concept you’ll likely explore is advanced data feeding, where you load data from external sources like CSV files to use in your requests, making your simulations more dynamic and representative of real-world data.

Want structured learning?

Take the full Gatling course →