The most surprising thing about Gatling’s HTTP DSL is that it’s not just for sending HTTP requests, but for understanding and testing the HTTP protocol itself, down to the nitty-gritty of how servers should respond.
Let’s see Gatling in action. Imagine we’re testing a simple API that returns user data. Here’s a basic simulation:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class BasicUserSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
val scn = scenario("User API Test")
.exec(
http("Get User 1")
.get("/users/1")
.check(status.is(200))
.check(jsonPath("$.id").is("1"))
.check(jsonPath("$.name").saveAs("userName"))
)
.exec(
http("Get User 2")
.get("/users/2")
.check(status.is(200))
.check(jsonPath("$.name").is("Alice"))
)
.exec(
http("Create User")
.post("/users")
.body(StringBody("""{"name": "Bob", "email": "bob@example.com"}"""))
.check(status.is(201))
.check(jsonPath("$.id").saveAs("newUserId"))
)
.exec(
http("Get Created User")
.get("/users/${newUserId}")
.check(status.is(200))
.check(jsonPath("$.name").is("Bob"))
)
setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol))
}
This simulation does a few things:
- Sets up the base URL and default headers:
httpProtocoldefines where our requests go and what common headers they send. - Defines a scenario:
scnis a sequence of actions. - Executes HTTP requests: Each
http(...)block defines a request. We specify the HTTP method (get,post), the path, optional request bodies, and crucially, assertions (check). - Asserts responses:
status.is(200)checks for a 200 OK status.jsonPath("$.id").is("1")verifies a specific value in the JSON response.jsonPath("$.name").saveAs("userName")extracts a value and stores it for later use. - Injects users and runs the simulation:
setUptells Gatling how many virtual users to simulate and how to run them.
The core problem Gatling solves is simulating realistic user load against an HTTP service. But its DSL goes deeper. It allows you to model not just the requests your users make, but the protocol-level interactions and the expected outcomes.
Internally, Gatling uses Netty for high-performance HTTP handling. The DSL you write is translated into a series of Netty HttpRequest objects and associated Response expectations. When a response comes back, Gatling checks it against your check assertions. These aren’t just simple "did it return 200?" checks; they are powerful assertions that can traverse JSON, XML, extract data, and even validate headers.
The check block is where the real power lies. You can use regex, css, xpath, jsonPath, md5, sha1, and more to extract data or validate content. For example, jsonPath("$.items[*].id").findAll.saveAs("itemIds") would find all id fields within any items array and store them. The status check is fundamental, but you can also check headers like header("Location").is("some/path") for redirects.
One aspect that trips many up is how Gatling handles dependent requests. When you saveAs a value, like newUserId in the example, it’s stored in Gatling’s Session object. Subsequent requests can then reference this session variable using ${variableName}. This is how you model stateful interactions, where one request’s output directly influences the next. It’s not just about hitting endpoints; it’s about simulating the flow of a user interacting with an application.
This system is designed to be highly configurable. You can define multiple httpProtocol configurations for different base URLs or authentication schemes. You can chain exec blocks to create complex flows, use repeat for loops, foreach for iterating over data, and randomSwitch or roundRobin to introduce variability in user behavior.
After fixing a common issue where your simulation fails because the server returns a 404 Not Found for a resource that should exist, you might then encounter errors related to incorrect JSON payload validation, where a field you expected to be a string is actually an integer.