Gatling simulations are compiled Scala code, not just configuration files, which is why you can build incredibly complex, dynamic load tests that perfectly model real-world user behavior.
Let’s see Gatling in action. Imagine we want to simulate users browsing an e-commerce site, adding items to their cart, and then checking out. This isn’t a simple, linear flow. Users might abandon carts, go back to browsing, or encounter errors.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class ComplexECommerceSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080") // Replace with your actual base URL
.inferHtmlResources()
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0")
val scn = scenario("E-Commerce Browsing and Checkout")
.exec(http("Homepage Load")
.get("/"))
.pause(1.seconds, 3.seconds) // Pause for 1 to 3 seconds
.group("Product Browsing") {
exec(http("Search Products")
.get("/products?query=gadget")
.check(status.is(200))
.check(css("a[href^='/product/']", "href").findAll.saveAs("productLinks")))
.pause(2.seconds, 5.seconds)
.doIf("${productLinks.count()} > 0") {
foreach("${productLinks}", "productLink") {
exec(http("View Product")
.get("${productLink}")
.check(css("input[name='add-to-cart']", "value").saveAs("addToCartButtonValue")))
.pause(1.seconds, 2.seconds)
}
}
}
.group("Add to Cart") {
exec(http("Add Item to Cart")
.post("/cart/add")
.formParam("productId", "${addToCartButtonValue}") // Example: using a captured value
.formParam("quantity", "1")
.check(status.is(200))
.check(jsonPath("$.cartId").saveAs("cartId")))
.pause(1.seconds, 3.seconds)
}
.group("Checkout Process") {
exec(http("View Cart")
.get("/cart/${cartId}") // Using the captured cartId
.check(status.is(200)))
.pause(2.seconds, 4.seconds)
.exec(http("Initiate Checkout")
.post("/checkout/initiate")
.formParam("cartId", "${cartId}")
.check(status.is(200))
.check(jsonPath("$.checkoutId").saveAs("checkoutId")))
.pause(1.seconds, 2.seconds)
.exec(http("Submit Order")
.post("/order/submit")
.formParam("checkoutId", "${checkoutId}")
.formParam("paymentMethod", "creditCard")
.check(status.in(200 to 201))) // Allow 200 OK or 201 Created
}
setUp(
scn.inject(
rampUsers(100) during (30.seconds), // Gradually ramp up to 100 users over 30 seconds
constantUsersPerSec(20) during (1.minute) // Maintain 20 users per second for 1 minute
).protocols(httpProtocol)
)
}
This simulation models several key aspects of user interaction:
- Navigation: It starts by loading the homepage, then searches for products.
- Dynamic Content: It extracts product links from the search results using
cssselectors and then iterates through them. This is crucial because you don’t know which products a user will click on beforehand. - State Management: It captures
productIdandcartIdusingsaveAsand then uses these values in subsequent requests. This mimics how a real browser maintains session state. - Conditional Logic:
doIfallows for branching. For instance, if no product links are found, the simulation won’t try to view products. - Varying User Behavior:
pauseintroduces realistic delays between actions, andrampUsersandconstantUsersPerSeccontrol the user load over time.
The problem this solves is creating load tests that go beyond simple API endpoint calls. Real users don’t just hit one URL; they navigate, interact with dynamic content, and their actions depend on previous responses. Gatling’s Scala DSL allows you to express these complex, multi-step user journeys precisely.
Internally, Gatling compiles this Scala code into efficient Akka actors. Each user’s virtual session is an actor, processing requests and managing its state. This actor-based architecture is highly scalable and allows Gatling to simulate hundreds of thousands of concurrent users on a single machine.
The check function is where the magic of dynamic simulation happens. You can use css, jsonPath, regex, and many other matchers to extract data from responses. This extracted data can then be used in subsequent requests, making your simulations adaptive. For example, css("a[href^='/product/']", "href").findAll.saveAs("productLinks") not only checks that the response contains links to products but also captures all those links into a list named productLinks for later use.
The group function is purely for reporting. It allows you to logically group related requests in the Gatling HTML report, making it easier to analyze performance for specific user actions like "Product Browsing" or "Add to Cart."
When you use foreach with a list of captured values, like foreach("${productLinks}", "productLink"), Gatling will execute the enclosed exec block for each item in the list. This is how you simulate a user interacting with multiple items discovered dynamically.
The setUp block defines how your simulation will be executed. You can specify injection profiles like rampUsers (gradually increasing users) and constantUsersPerSec (maintaining a steady rate). Combining these allows for sophisticated load profiles that mimic real-world traffic patterns, such as a sudden surge during a flash sale followed by a steady state.
The inferHtmlResources() method is a convenience that automatically detects and requests static assets (CSS, JS, images) linked in the HTML, making your simulation more realistic by including the load of rendering a web page.
The most surprising thing about Gatling simulations is how closely they mirror actual application code, allowing for sophisticated control flow and data manipulation that simply isn’t possible with declarative tools. You’re writing behavior, not just configuration.
The next concept you’ll want to explore is advanced error handling and custom feeders for injecting large, realistic datasets.