Gatling simulations are often static, but real-world scenarios demand dynamic data. CSV and JSON feeders let you inject this data, turning a single script into a thousand test cases.
Let’s see it in action. Imagine we’re testing a simple API endpoint that accepts a userId. Instead of hardcoding userId=123 in our Gatling script, we want to feed it a list of user IDs from a file.
Here’s a snippet of a users.csv file:
userId,username
101,alice
102,bob
103,charlie
And here’s how you’d use it in your Gatling simulation:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class UserSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
val csvFeeder = csv("users.csv").circular // Use .circular to loop indefinitely
val scn = scenario("User API Test")
.feed(csvFeeder) // Inject data from the feeder
.exec(http("Get User")
.get("/users/${userId}") // Use the injected userId
.check(jsonPath("$.username").is("${username}"))) // And the username
setUp(
scn.inject(rampUsers(100).during(10.seconds))
).protocols(httpProtocol)
}
When this simulation runs, Gatling will pick up the first row from users.csv (userId=101, username=alice) and use it for the first request. The next request will use the second row (userId=102, username=bob), and so on. Because we used .circular, once Gatling reaches the end of the CSV, it will loop back to the beginning, reusing the data.
This simple feed statement is the core mechanism. Gatling takes the column names from the CSV (or keys from JSON) and makes them available as session variables. You then reference these variables within your HTTP requests, checks, or even in other feeders, using the ${variableName} syntax.
The power comes from the variety of feeders available and how they can be combined. You aren’t limited to simple CSVs. JSON feeders are equally powerful:
users.json:
[
{"userId": 201, "role": "admin"},
{"userId": 202, "role": "guest"}
]
And in your simulation:
val jsonFeeder = jsonFile("users.json").circular
val scnJson = scenario("User Role Test")
.feed(jsonFeeder)
.exec(http("Get User Role")
.get("/users/${userId}/role")
.check(jsonPath("$.role").is("${role}")))
Here, jsonFile parses the JSON array, and each object becomes a set of session variables.
You can also chain feeders. Suppose you have a list of userIds in one file and a list of actions in another. You can feed them sequentially:
actions.csv:
action
view
edit
delete
val userFeeder = csv("users.csv").queue // Use .queue to consume each entry once
val actionFeeder = csv("actions.csv").circular
val scnChained = scenario("User Action Test")
.feed(userFeeder) // First, get a user
.feed(actionFeeder) // Then, get an action
.exec(http("Perform User Action")
.post("/users/${userId}/actions")
.body(StringBody("""{"action": "${action}"}""")))
This simulation will pair each user sequentially with actions, looping through actions for each user. If users.csv has 3 users and actions.csv has 3 actions, you’ll get 3 requests. If actions.csv had 5 actions, you’d get 5 requests for the first user, then 5 for the second, and so on, as the actionFeeder cycles.
The .queue strategy consumes each record from the feeder once, then stops. .circular repeats the feeder’s data indefinitely. .random picks a random record each time. .shuffle reads all records, shuffles them, and then queues them. The choice depends entirely on your test’s needs: do you need to test each user with a specific set of actions, or just a random combination?
When you use feeders, Gatling doesn’t just read a file; it loads the data into memory (or uses a streaming approach for very large files) and manages it as part of the simulation’s state. Each virtual user gets its own copy of the current feeder state and advances its pointer independently. This is why strategies like .circular and .queue are crucial for managing how these pointers are updated across many users. If you have 100 users and a feeder with 10 items, .circular means every user will eventually get data from item 1, item 2, etc., repeatedly. .queue means only the first 10 users will get distinct items, and subsequent users will receive nothing from that feeder if it’s not circular.
The most surprising part is how easily feeders interact with Gatling’s El (Expression Language). You can use the output of one feeder as input for another, or even use Gatling’s built-in functions within your feeder data. For example, you could have a timestamp feeder and use its output directly in a request body.
You’ll next want to explore how to combine multiple feeders with different strategies and how to dynamically generate data within a simulation itself, rather than relying solely on external files.