GraphQL endpoints are surprisingly hard to load test effectively with standard tools, but Gatling handles them beautifully if you know how.
Let’s say you’ve got a GraphQL API running at http://localhost:8080/graphql. Your clients are sending POST requests to this endpoint with a JSON body like this:
{
"query": "query GetUser($id: ID!) { user(id: $id) { name email } }",
"variables": { "id": "123" }
}
Here’s how you’d set up a Gatling simulation to hammer it.
First, you need the Gatling core dependency. If you’re using Maven:
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>3.11.5</version> <!-- Use the latest stable version -->
<scope>test</scope>
</dependency>
Or for Gradle:
testImplementation 'io.gatling.highcharts:gatling-charts-highcharts:3.11.5' // Use the latest stable version
Now, let’s write the simulation. You’ll create a Scala file in your src/test/scala directory, say GraphQLSimulation.scala.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class GraphQLSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080") // Your GraphQL endpoint's base URL
.acceptHeader("application/json")
.contentTypeHeader("application/json") // Crucial for GraphQL POST requests
// Define your GraphQL query and variables
val query = """
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
"""
val variables = """
{ "id": "123" }
"""
// Create a JSON body for the request
val gqlBody = s"""
{
"query": "$query",
"variables": $variables
}
"""
val scn = scenario("GraphQL User Query")
.exec(
http("Request User Data")
.post("/graphql") // Your GraphQL endpoint path
.body(StringBody(gqlBody))
.check(status.is(200)) // Basic check: ensure the request was successful
// Add more specific checks for your response if needed
// .check(jsonPath("$.data.user.name").exists)
)
setUp(
scn.inject(
rampUsers(100) during (30 seconds), // Gradually ramp up to 100 users over 30 seconds
constantUsersPerSec(50) during (1 minute) // Maintain 50 users per second for 1 minute
).protocols(httpProtocol)
)
}
This simulation does a few key things. It sets the baseUrl, importantly configures contentTypeHeader to application/json, and then crafts the exact JSON payload that a GraphQL client would send. The StringBody is essential because Gatling needs to send the raw JSON string as the request body. The post("/graphql") targets your specific endpoint, and check(status.is(200)) is a minimal validation. The setUp block defines how the load is applied, starting with a ramp-up and then holding a steady state.
When you run this, Gatling will generate detailed reports showing response times, throughput, and error rates. You can see how your GraphQL server performs under load, identify bottlenecks, and tune your queries or server configuration.
The real power comes when you start parameterizing your requests. Instead of a hardcoded ID, you can fetch IDs from a feeder:
val csvFeeder = csv("users.csv").random // Assumes users.csv contains an 'id' column
val queryWithParam = """
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
"""
val scnParametrized = scenario("GraphQL User Query with Feeder")
.feed(csvFeeder) // Inject data from the feeder
.exec(
http("Request User Data")
.post("/graphql")
.body(StringBody(session => {
val variablesParam = s"""{ "id": "${session("id").as[String]}" }"""
s"""
{
"query": "$queryWithParam",
"variables": $variablesParam
}
"""
}))
.check(status.is(200))
)
// ... (setUp block would use scnParametrized)
In this snippet, users.csv would look like:
id
1
2
3
...
The StringBody(session => { ... }) part is crucial. It’s a function that receives the current session object, allowing you to dynamically construct the request body using values from feeders (like session("id").as[String]). This simulates real-world scenarios where different users or requests need different parameters.
The most people miss is that Gatling’s json() body support is designed for REST APIs where you’re sending a JSON object as the entire body. For GraphQL, the entire body is a JSON object containing your query and variables. You’re not just sending {"id": "123"}, you’re sending {"query": "...", "variables": {"id": "123"}}. This distinction means StringBody is almost always the way to go for GraphQL, letting you construct the precise JSON payload expected by the GraphQL endpoint.
The next hurdle is handling authentication and authorization headers dynamically within your parameterized requests.