Gatling’s Session isn’t just a passive container; it’s an active participant in your test, holding and transforming data as requests flow.
Let’s see it in action. Imagine you’re testing an API that requires a user ID, and you want to dynamically fetch that ID from a previous response and use it in subsequent requests.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class DynamicDataSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080") // Replace with your actual base URL
.acceptHeader("application/json")
val scn = scenario("Dynamic Data Usage")
.exec(
http("Create User")
.post("/users")
.body(StringBody("""{"name": "Test User"}""")).asJson
.check(status.is(201))
.check(jsonPath("$.id").saveAs("userId")) // Save the created user ID into the session
)
.exec { session => // Log the userId to demonstrate it's in the session
println(s"User ID from session: ${session.get("userId").asOption[String]}")
session
}
.exec(
http("Get User")
.get("/users/${userId}") // Use the userId from the session
.check(status.is(200))
.check(jsonPath("$.name").is("Test User"))
)
.exec(
http("Update User")
.put("/users/${userId}") // Use the userId again
.body(StringBody("""{"name": "Updated Test User"}""")).asJson
.check(status.is(200))
.check(jsonPath("$.name").is("Updated Test User"))
)
setUp(
scn.inject(rampUsers(10) during (10.seconds))
).protocols(httpProtocol)
}
In this simulation, the Create User request returns a JSON payload containing the newly created user’s ID. The .check(jsonPath("$.id").saveAs("userId")) line is where the magic happens: it extracts the value from the $.id JSON path and stores it in the Gatling Session under the key "userId".
From that point forward, any subsequent HTTP request can reference this userId using the ${userId} EL (Expression Language) syntax. Gatling automatically resolves these placeholders by looking up the corresponding value in the current Session for that virtual user. This allows you to chain requests where the output of one directly influences the input of another, simulating realistic user flows.
The Session is a thread-local object for each virtual user. This means data stored in a Session is isolated to that specific user’s execution thread. When you use .saveAs("key"), you’re adding a key-value pair to this thread-local map. Later, ${key} tells Gatling to look up the value associated with key in the current thread’s Session. If the key isn’t found, it typically results in an error or an empty value, depending on the context.
You can store almost any Scala type in the session, not just strings. For instance, you could save a computed integer, a list, or even a custom object. The session.get("key") method returns an Option[Any], so you’ll often need to use .as[T] or .asOption[T] to cast and retrieve the value safely. You can also perform complex transformations within exec { session => ... } blocks, manipulating the session data before passing it to subsequent requests. This is powerful for pre-processing data, performing calculations, or setting up complex state for your tests.
The most surprising thing about Gatling’s session is how it manages state across complex scenarios and multiple users without explicit synchronization. Each user’s session is a distinct, immutable object. When you "update" a session (e.g., by saving a value), you’re actually creating a new session object with the added data, which is then passed to the next step. This functional approach simplifies reasoning about state and avoids common concurrency pitfalls, making your simulations more robust.
The next concept you’ll want to explore is how to handle conditional logic based on session data, allowing your simulations to adapt to different response scenarios.