Overview
Gatling is a powerful open-source load testing tool for web applications. While written in Scala, it provides excellent Java DSL support for creating performance tests.
Dependencies
Maven
<properties>
<gatling.version>3.9.5</gatling.version>
<gatling-maven-plugin.version>4.5.2</gatling-maven-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>io.gatling</groupId>
<artifactId>gatling-core</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.gatling</groupId>
<artifactId>gatling-http</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling-maven-plugin.version}</version>
<configuration>
<simulationsFolder>src/test/java</simulationsFolder>
</configuration>
</plugin>
</plugins>
</build>
Gradle
plugins {
id 'io.gatling.gradle' version '3.9.5'
}
dependencies {
gatling 'io.gatling:gatling-core:3.9.5'
gatling 'io.gatling:gatling-http:3.9.5'
gatling 'io.gatling.highcharts:gatling-charts-highcharts:3.9.5'
}
Basic Gatling Simulation
1. Simple HTTP Load Test
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import java.time.Duration;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
public class BasicSimulation extends Simulation {
// HTTP protocol configuration
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://jsonplaceholder.typicode.com")
.acceptHeader("application/json")
.userAgentHeader("Gatling Performance Test");
// Scenario definition
ScenarioBuilder scn = scenario("Basic API Test")
.exec(
http("Get all posts")
.get("/posts")
.check(status().is(200))
.check(jsonPath("$[0].id").saveAs("firstPostId"))
)
.pause(1) // Pause for 1 second
.exec(
http("Get specific post")
.get("/posts/#{firstPostId}")
.check(status().is(200))
.check(jsonPath("$.title").saveAs("postTitle"))
)
.exec(session -> {
System.out.println("Post Title: " + session.getString("postTitle"));
return session;
});
// Simulation setup
{
setUp(
scn.injectOpen(
nothingFor(5), // Wait 5 seconds
atOnceUsers(10), // Inject 10 users at once
rampUsers(50).during(30) // Ramp up to 50 users over 30 seconds
)
).protocols(httpProtocol);
}
}
2. Advanced Simulation with Multiple Scenarios
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import java.util.concurrent.ThreadLocalRandom;
import java.time.Duration;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
public class AdvancedApiSimulation extends Simulation {
// Headers
private static final Map<CharSequence, String> HEADERS_JSON = Map.of(
"Content-Type", "application/json",
"Accept", "application/json"
);
// HTTP Protocol
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://api.example.com")
.headers(HEADERS_JSON)
.acceptHeader("application/json")
.userAgentHeader("Gatling/3.9.5")
.disableCaching()
.disableFollowRedirect();
// Feeder for test data
FeederBuilder<String> userFeeder = csv("data/users.csv").random();
FeederBuilder<String> productFeeder = csv("data/products.csv").circular();
// Common HTTP calls
ChainBuilder authenticate =
exec(
http("Authenticate User")
.post("/auth/login")
.body(ElFileBody("bodies/login.json"))
.check(status().is(200))
.check(jsonPath("$.token").saveAs("authToken"))
);
ChainBuilder getAllProducts =
exec(
http("Get All Products")
.get("/products")
.header("Authorization", "Bearer #{authToken}")
.check(status().is(200))
.check(jsonPath("$[*].id").findRandom().saveAs("randomProductId"))
);
ChainBuilder getProductDetail =
exec(
http("Get Product Detail")
.get("/products/#{randomProductId}")
.header("Authorization", "Bearer #{authToken}")
.check(status().is(200))
.check(jsonPath("$.price").saveAs("productPrice"))
);
ChainBuilder createOrder =
exec(
http("Create Order")
.post("/orders")
.header("Authorization", "Bearer #{authToken}")
.body(ElFileBody("bodies/create_order.json"))
.check(status().is(201))
.check(jsonPath("$.orderId").saveAs("orderId"))
);
ChainBuilder checkoutOrder =
exec(
http("Checkout Order")
.post("/orders/#{orderId}/checkout")
.header("Authorization", "Bearer #{authToken}")
.check(status().is(200))
);
// Scenarios
ScenarioBuilder browsingUsers = scenario("Browsing Users")
.feed(userFeeder)
.exec(authenticate)
.pause(2)
.exec(getAllProducts)
.pause(1, 5) // Random pause between 1-5 seconds
.exec(getProductDetail)
.pause(2)
.repeat(3, "viewCount") { // View 3 random products
exec(getAllProducts)
.exec(getProductDetail)
.pause(1)
};
ScenarioBuilder purchasingUsers = scenario("Purchasing Users")
.feed(userFeeder)
.feed(productFeeder)
.exec(authenticate)
.pause(1)
.exec(createOrder)
.pause(2)
.exec(checkoutOrder);
// Setup simulation
{
setUp(
browsingUsers.injectOpen(
rampUsersPerSec(1).to(10).during(60), // Ramp up from 1 to 10 users/sec over 1 minute
constantUsersPerSec(10).during(120) // Maintain 10 users/sec for 2 minutes
),
purchasingUsers.injectOpen(
constantUsersPerSec(2).during(180) // 2 purchases per second for 3 minutes
)
)
.protocols(httpProtocol)
.maxDuration(Duration.ofMinutes(5))
.assertions(
global().responseTime().percentile3().lte(500), // 95% of requests < 500ms
global().successfulRequests().percent().gte(99.0), // Success rate >= 99%
forAll().responseTime().max().lte(2000) // All requests < 2 seconds
);
}
}
Test Data Files
1. users.csv
username,password,email user1,pass123,[email protected] user2,pass456,[email protected] user3,pass789,[email protected]
2. products.csv
productId,productName 1,Laptop 2,Smartphone 3,Tablet 4,Headphones
3. bodies/login.json
{
"username": "${username}",
"password": "${password}"
}
4. bodies/create_order.json
{
"productId": "${productId}",
"quantity": 1,
"customerEmail": "${email}"
}
Complex Simulation Examples
Example 1: E-commerce Load Test
public class EcommerceSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://api.ecommerce.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.disableCaching()
.shareConnections();
// Feeders
FeederBuilder.Batchable<String> userFeeder =
csv("data/ecommerce_users.csv").circular();
FeederBuilder.Batchable<String> productFeeder =
csv("data/products.csv").random();
FeederBuilder.Batchable<String> searchFeeder =
csv("data/search_terms.csv").random();
// Headers with authentication
private Map<CharSequence, String> authHeaders(String token) {
return Map.of(
"Authorization", "Bearer " + token,
"Content-Type", "application/json"
);
}
// Actions
ChainBuilder login =
exec(
http("Login")
.post("/api/v1/auth/login")
.body(StringBody("""
{
"email": "${email}",
"password": "${password}"
}
"""))
.check(status().is(200))
.check(jsonPath("$.accessToken").saveAs("accessToken"))
.check(jsonPath("$.refreshToken").saveAs("refreshToken"))
);
ChainBuilder browseProducts =
exec(
http("Browse Products")
.get("/api/v1/products")
.queryParam("page", "0")
.queryParam("size", "20")
.queryParam("sort", "name,asc")
.check(status().is(200))
.check(jsonPath("$.content[0].id").saveAs("firstProductId"))
.check(jsonPath("$.content[0].name").saveAs("firstProductName"))
);
ChainBuilder searchProducts =
exec(
http("Search Products")
.get("/api/v1/products/search")
.queryParam("q", "${searchTerm}")
.queryParam("category", "${category}")
.check(status().is(200))
.check(jsonPath("$.content[*].id").findRandom().saveAs("searchedProductId"))
);
ChainBuilder viewProduct =
exec(
http("View Product Details")
.get("/api/v1/products/#{productId}")
.check(status().is(200))
.check(jsonPath("$.price").saveAs("productPrice"))
.check(jsonPath("$.inStock").saveAs("inStock"))
);
ChainBuilder addToCart =
exec(
http("Add to Cart")
.post("/api/v1/cart/items")
.headers(session -> authHeaders(session.getString("accessToken")))
.body(StringBody("""
{
"productId": "${productId}",
"quantity": 1
}
"""))
.check(status().is(200))
.check(jsonPath("$.cartItemId").saveAs("cartItemId"))
);
ChainBuilder viewCart =
exec(
http("View Cart")
.get("/api/v1/cart")
.headers(session -> authHeaders(session.getString("accessToken")))
.check(status().is(200))
.check(jsonPath("$.totalItems").saveAs("cartItemCount"))
);
ChainBuilder checkout =
exec(
http("Checkout")
.post("/api/v1/orders")
.headers(session -> authHeaders(session.getString("accessToken")))
.body(StringBody("""
{
"shippingAddress": {
"street": "123 Main St",
"city": "Test City",
"zipCode": "12345"
},
"paymentMethod": "CREDIT_CARD"
}
"""))
.check(status().is(201))
.check(jsonPath("$.orderId").saveAs("orderId"))
.check(jsonPath("$.orderStatus").is("PENDING"))
);
ChainBuilder getOrderStatus =
exec(
http("Get Order Status")
.get("/api/v1/orders/#{orderId}")
.headers(session -> authHeaders(session.getString("accessToken")))
.check(status().is(200))
.check(jsonPath("$.status").saveAs("orderStatus"))
);
// Scenarios
ScenarioBuilder anonymousBrowser = scenario("Anonymous Browser")
.feed(searchFeeder)
.exec(browseProducts)
.pause(2, 5)
.exec(searchProducts)
.pause(1, 3)
.exec(session -> {
String productId = session.getString("searchedProductId");
return session.set("productId", productId);
})
.exec(viewProduct)
.pause(2);
ScenarioBuilder registeredUser = scenario("Registered User")
.feed(userFeeder)
.feed(productFeeder)
.exec(login)
.pause(1)
.exec(browseProducts)
.pause(1, 3)
.exec(viewProduct)
.pause(1)
.exec(addToCart)
.pause(1)
.exec(viewCart)
.pause(2)
.doIf(session -> {
int itemCount = session.getInt("cartItemCount");
return itemCount > 0;
}).then(
exec(checkout)
.pause(3)
.exec(getOrderStatus)
);
ScenarioBuilder heavyBuyer = scenario("Heavy Buyer")
.feed(userFeeder)
.exec(login)
.pause(1)
.repeat(5, "purchaseCount") {
feed(productFeeder)
.exec(viewProduct)
.exec(addToCart)
.pause(1)
}
.exec(viewCart)
.exec(checkout)
.exec(getOrderStatus);
// Setup
{
setUp(
anonymousBrowser.injectOpen(
constantUsersPerSec(5).during(300) // 5 anonymous users/sec for 5 minutes
),
registeredUser.injectOpen(
rampUsersPerSec(1).to(10).during(180), // Ramp up to 10 registered users/sec
constantUsersPerSec(10).during(420) // Maintain for 7 minutes
),
heavyBuyer.injectOpen(
constantUsersPerSec(2).during(300) // 2 heavy buyers/sec for 5 minutes
)
)
.protocols(httpProtocol)
.maxDuration(Duration.ofMinutes(10))
.assertions(
global().responseTime().percentile3().lte(800),
global().successfulRequests().percent().gte(99.5),
details("Checkout").responseTime().percentile3().lte(1000),
details("Login").responseTime().max().lte(1500)
);
}
}
Example 2: API Gateway & Microservices Test
public class MicroservicesSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://api-gateway.company.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.disableCaching()
.warmUp("https://api-gateway.company.com/health")
.connectionHeader("keep-alive");
// Feeders
FeederBuilder.Batchable<String> customerFeeder =
csv("data/customers.csv").circular();
FeederBuilder.Batchable<String> transactionFeeder =
csv("data/transactions.csv").random();
// Headers
private Map<CharSequence, String> apiHeaders(String apiKey) {
return Map.of(
"X-API-Key", apiKey,
"X-Request-ID", "#{randomUUID}",
"Content-Type", "application/json"
);
}
// Helper to generate random UUID
private static String randomUUID() {
return java.util.UUID.randomUUID().toString();
}
// Chain builders for different services
ChainBuilder getUserProfile =
exec(session -> session.set("randomUUID", randomUUID()))
.exec(
http("Get User Profile")
.get("/user-service/v1/users/${userId}")
.headers(session -> apiHeaders(session.getString("apiKey")))
.check(status().is(200))
.check(jsonPath("$.user.email").saveAs("userEmail"))
);
ChainBuilder getAccountBalance =
exec(session -> session.set("randomUUID", randomUUID()))
.exec(
http("Get Account Balance")
.get("/account-service/v1/accounts/${accountId}/balance")
.headers(session -> apiHeaders(session.getString("apiKey")))
.check(status().is(200))
.check(jsonPath("$.balance").saveAs("accountBalance"))
);
ChainBuilder processPayment =
exec(session -> session.set("randomUUID", randomUUID()))
.exec(
http("Process Payment")
.post("/payment-service/v1/payments")
.headers(session -> apiHeaders(session.getString("apiKey")))
.body(StringBody("""
{
"fromAccount": "${fromAccount}",
"toAccount": "${toAccount}",
"amount": ${amount},
"currency": "USD"
}
"""))
.check(status().is(201))
.check(jsonPath("$.paymentId").saveAs("paymentId"))
.check(jsonPath("$.status").is("COMPLETED"))
);
ChainBuilder getTransactionHistory =
exec(session -> session.set("randomUUID", randomUUID()))
.exec(
http("Get Transaction History")
.get("/transaction-service/v1/transactions")
.queryParam("accountId", "${accountId}")
.queryParam("page", "0")
.queryParam("size", "10")
.headers(session -> apiHeaders(session.getString("apiKey")))
.check(status().is(200))
.check(jsonPath("$.content").exists())
);
ChainBuilder sendNotification =
exec(session -> session.set("randomUUID", randomUUID()))
.exec(
http("Send Notification")
.post("/notification-service/v1/notifications")
.headers(session -> apiHeaders(session.getString("apiKey")))
.body(StringBody("""
{
"userId": "${userId}",
"type": "TRANSACTION_ALERT",
"message": "Payment of ${amount} processed successfully",
"channel": "EMAIL"
}
"""))
.check(status().is(202))
);
// Complex business workflow
ChainBuilder moneyTransferWorkflow =
exec(session -> session.set("randomUUID", randomUUID()))
.exec(getUserProfile)
.pause(1)
.exec(getAccountBalance)
.pause(1)
.doIf(session -> {
double balance = session.getDouble("accountBalance");
double amount = session.getDouble("amount");
return balance >= amount;
}).then(
exec(processPayment)
.pause(2)
.exec(sendNotification)
.exec(getTransactionHistory)
).orElse(
exec(
http("Insufficient Funds")
.post("/notification-service/v1/notifications")
.headers(session -> apiHeaders(session.getString("apiKey")))
.body(StringBody("""
{
"userId": "${userId}",
"type": "LOW_BALANCE_ALERT",
"message": "Insufficient funds for transfer",
"channel": "SMS"
}
"""))
.check(status().is(202))
)
);
// Scenarios
ScenarioBuilder userProfileReaders = scenario("Profile Readers")
.feed(customerFeeder)
.during(300).on( // Run for 5 minutes
randomSwitch().on(
Choice.withWeight(70, exec(getUserProfile).pause(2, 5)),
Choice.withWeight(30, exec(getAccountBalance).pause(1, 3))
)
);
ScenarioBuilder transactionProcessors = scenario("Transaction Processors")
.feed(customerFeeder)
.feed(transactionFeeder)
.exec(moneyTransferWorkflow)
.pause(5, 10);
ScenarioBuilder mixedWorkload = scenario("Mixed Workload")
.feed(customerFeeder)
.feed(transactionFeeder)
.forever().on(
randomSwitch().on(
Choice.withWeight(40, exec(getUserProfile)),
Choice.withWeight(25, exec(getAccountBalance)),
Choice.withWeight(20, exec(getTransactionHistory)),
Choice.withWeight(15, exec(moneyTransferWorkflow))
).pause(1, 5)
);
// Setup with throttling
{
setUp(
userProfileReaders.injectOpen(
rampUsers(100).during(60) // 100 users over 1 minute
),
transactionProcessors.injectOpen(
constantUsersPerSec(5).during(300) // 5 transactions/sec for 5 minutes
),
mixedWorkload.injectOpen(
rampUsersPerSec(1).to(20).during(120), // Ramp up to 20 requests/sec
constantUsersPerSec(20).during(480) // Maintain for 8 minutes
)
)
.protocols(httpProtocol)
.throttle(
reachRps(100).in(60), // Reach 100 RPS in 1 minute
holdFor(480), // Hold for 8 minutes
jumpToRps(50), // Jump to 50 RPS
holdFor(120) // Hold for 2 minutes
)
.maxDuration(Duration.ofMinutes(15))
.assertions(
global().responseTime().percentile3().lte(300),
global().successfulRequests().percent().gte(99.9),
details("Process Payment").responseTime().mean().lte(500),
details("Get User Profile").responseTime().percentile4().lte(200)
);
}
}
Advanced Features
1. Custom Checks and Assertions
public class AdvancedChecksSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json");
// Custom response time check
ChainBuilder checkResponseTime =
exec(
http("API Call")
.get("/api/data")
.check(
status().is(200),
responseTimeInMillis().lt(1000).saveAs("responseTime"),
bodyString().transform(body -> {
// Custom validation logic
if (body.contains("error")) {
throw new RuntimeException("Response contains error");
}
return body;
})
)
);
// JSON schema validation
ChainBuilder validateJsonSchema =
exec(
http("Get Structured Data")
.get("/api/structured")
.check(
jsonPath("$.id").is("#{expectedId}"),
jsonPath("$.name").notNull(),
jsonPath("$.items").count().gte(1),
jsonPath("$.metadata.timestamp").ofType(Long.class).gt(0)
)
);
// Session validation
ChainBuilder validateSessionData =
exec(
http("Get Data")
.get("/api/data")
.check(
jsonPath("$.results[*].id").findAll().saveAs("allIds")
)
)
.exec(session -> {
List<String> allIds = session.getList("allIds");
if (allIds.isEmpty()) {
throw new RuntimeException("No IDs found in response");
}
return session;
});
ScenarioBuilder scn = scenario("Advanced Checks")
.exec(checkResponseTime)
.exec(validateJsonSchema)
.exec(validateSessionData);
{
setUp(
scn.injectOpen(atOnceUsers(10))
).protocols(httpProtocol);
}
}
2. Data-Driven Testing with Feeds
public class DataDrivenSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http
.baseUrl("https://jsonplaceholder.typicode.com");
// Different feeder types
FeederBuilder.Batchable<String> csvFeeder =
csv("data/test_data.csv").random();
FeederBuilder.Batchable<String> jsonFeeder =
jsonFile("data/test_data.json").random();
FeederBuilder.Batchable<String> ssvFeeder =
ssv("data/test_data.ssv").circular();
// Custom feeder from code
Iterator<Map<String, Object>> customFeeder =
new Iterator<Map<String, Object>>() {
private int counter = 0;
@Override
public boolean hasNext() {
return true;
}
@Override
public Map<String, Object> next() {
return Map.of(
"userId", counter++,
"username", "user" + counter,
"email", "user" + counter + "@test.com"
);
}
};
ChainBuilder createUser =
feed(customFeeder)
.exec(
http("Create User #{userId}")
.post("/users")
.body(StringBody("""
{
"name": "${username}",
"email": "${email}",
"username": "user#{userId}"
}
"""))
.check(status().is(201))
.check(jsonPath("$.id").saveAs("newUserId"))
);
ChainBuilder updateUser =
exec(
http("Update User")
.put("/users/#{newUserId}")
.body(StringBody("""
{
"name": "Updated User",
"email": "[email protected]"
}
"""))
.check(status().is(200))
);
ScenarioBuilder scn = scenario("Data Driven Test")
.exec(createUser)
.pause(1)
.exec(updateUser);
{
setUp(
scn.injectOpen(
constantUsersPerSec(2).during(60) // 2 users/sec for 1 minute
)
).protocols(httpProtocol);
}
}
Running Gatling Tests
1. Maven Commands
# Run all Gatling tests mvn gatling:test # Run specific simulation mvn gatling:test -Dgatling.simulationClass=BasicSimulation # Run with custom properties mvn gatling:test -DbaseUrl=https://staging.example.com -Dusers=100
2. Gradle Commands
# Run all Gatling tests ./gradlew gatlingRun # Run specific simulation ./gradlew gatlingRun-AdvancedApiSimulation # Run with custom properties ./gradlew gatlingRun -DbaseUrl=https://staging.example.com
3. Programmatic Execution
public class GatlingRunner {
public static void main(String[] args) {
Engine
.engine()
.resourcesDirectory(IDEPathHelper.gradleResourcesDirectory.toString())
.resultsDirectory(IDEPathHelper.resultsDirectory.toString())
.reportsDirectory(IDEPathHelper.reportsDirectory.toString())
.run();
}
}
Results Analysis
Gatling generates comprehensive HTML reports including:
- Response Time Distribution
- Requests per Second
- Active Users Over Time
- Error Statistics
- Percentile Response Times
Best Practices
- Start Simple: Begin with basic scenarios and gradually add complexity
- Use Realistic Data: Feed realistic test data that matches production
- Simulate Real User Behavior: Include think times and random pauses
- Test Gradually: Start with low load and gradually increase
- Monitor Application: Correlate Gatling results with application metrics
- Use Assertions: Define performance requirements in your simulations
- Run Consistently: Use the same environment for comparable results
- Document Scenarios: Clearly document what each scenario represents
This comprehensive Gatling setup provides robust performance testing capabilities for Java applications, from simple API tests to complex microservices architectures.