Locust with Java Backend Integration

Comprehensive Locust Integration Guide for Java Applications

1. Locust Test Configuration and Setup

Maven Dependencies

<!-- pom.xml -->
<properties>
<locust4j.version>1.0.7</locust4j.version>
<testcontainers.version>1.19.3</testcontainers.version>
<micrometer.version>1.11.5</micrometer.version>
</properties>
<dependencies>
<!-- Locust4J - Java client for Locust -->
<dependency>
<groupId>com.github.myzhan</groupId>
<artifactId>locust4j</artifactId>
<version>${locust4j.version}</version>
</dependency>
<!-- Test Containers for Locust -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
</dependencies>

2. Locust Test Runner Service

// LocustTestRunner.java
package com.example.locust.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Service
public class LocustTestRunner {
private static final Logger logger = LoggerFactory.getLogger(LocustTestRunner.class);
@Value("${locust.scripts.directory:src/test/locust}")
private String scriptsDirectory;
@Value("${locust.results.directory:target/locust-results}")
private String resultsDirectory;
@Value("${locust.python.path:python3}")
private String pythonPath;
private final ExecutorService executorService = Executors.newCachedThreadPool();
public TestExecutionResult runLocustTest(String testScript, LocustOptions options) {
try {
Path scriptPath = Paths.get(scriptsDirectory, testScript);
Path resultsPath = Paths.get(resultsDirectory, 
testScript.replace(".py", "-" + System.currentTimeMillis()));
Files.createDirectories(resultsPath.getParent());
List<String> command = buildLocustCommand(scriptPath, resultsPath, options);
logger.info("Executing Locust test: {}", String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(new File("."));
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// Capture output
String output = captureOutput(process.getInputStream());
// Wait for process with timeout
boolean completed = process.waitFor(options.getTimeoutMinutes(), TimeUnit.MINUTES);
TestExecutionResult result = new TestExecutionResult();
result.setScriptName(testScript);
result.setOutput(output);
result.setResultsDirectory(resultsPath.toString());
if (completed) {
int exitCode = process.exitValue();
result.setExitCode(exitCode);
result.setSuccess(exitCode == 0);
logger.info("Locust test completed with exit code: {}", exitCode);
} else {
process.destroyForcibly();
result.setExitCode(-1);
result.setSuccess(false);
result.setTimeout(true);
logger.warn("Locust test timed out after {} minutes", options.getTimeoutMinutes());
}
return result;
} catch (IOException | InterruptedException e) {
logger.error("Failed to execute Locust test: {}", testScript, e);
throw new RuntimeException("Locust test execution failed", e);
}
}
public CompletableFuture<TestExecutionResult> runLocustTestAsync(String testScript, LocustOptions options) {
return CompletableFuture.supplyAsync(() -> runLocustTest(testScript, options), executorService);
}
public TestExecutionResult runLocustWithDocker(String testScript, LocustOptions options) {
try {
List<String> command = new ArrayList<>();
command.add("docker");
command.add("run");
command.add("--rm");
command.add("-v");
command.add(Paths.get(scriptsDirectory).toAbsolutePath() + ":/locust-scripts");
command.add("-v");
command.add(Paths.get(resultsDirectory).toAbsolutePath() + ":/locust-results");
command.add("locustio/locust");
command.add("-f");
command.add("/locust-scripts/" + testScript);
command.add("--host=" + options.getHost());
command.add("--users=" + options.getNumUsers());
command.add("--spawn-rate=" + options.getSpawnRate());
command.add("--run-time=" + options.getRunTime());
command.add("--headless");
command.add("--csv=/locust-results/" + testScript.replace(".py", ""));
if (options.isWebUI()) {
command.add("--web-host=0.0.0.0");
command.add("--web-port=8089");
}
logger.info("Executing Locust test with Docker: {}", String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(new File("."));
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
String output = captureOutput(process.getInputStream());
boolean completed = process.waitFor(options.getTimeoutMinutes(), TimeUnit.MINUTES);
TestExecutionResult result = new TestExecutionResult();
result.setScriptName(testScript);
result.setOutput(output);
if (completed) {
result.setExitCode(process.exitValue());
result.setSuccess(result.getExitCode() == 0);
} else {
process.destroyForcibly();
result.setExitCode(-1);
result.setSuccess(false);
result.setTimeout(true);
}
return result;
} catch (IOException | InterruptedException e) {
logger.error("Failed to execute Locust test with Docker: {}", testScript, e);
throw new RuntimeException("Locust Docker test execution failed", e);
}
}
private List<String> buildLocustCommand(Path scriptPath, Path resultsPath, LocustOptions options) {
List<String> command = new ArrayList<>();
command.add(pythonPath);
command.add("-m");
command.add("locust");
command.add("-f");
command.add(scriptPath.toString());
command.add("--host=" + options.getHost());
command.add("--users=" + options.getNumUsers());
command.add("--spawn-rate=" + options.getSpawnRate());
command.add("--run-time=" + options.getRunTime());
command.add("--headless");
command.add("--csv=" + resultsPath.toString());
command.add("--html=" + resultsPath + ".html");
command.add("--json=" + resultsPath + ".json");
if (options.isOnlySummary()) {
command.add("--only-summary");
}
return command;
}
private String captureOutput(InputStream inputStream) throws IOException {
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
logger.debug("Locust output: {}", line);
}
}
return output.toString();
}
public static class LocustOptions {
private String host = "http://localhost:8080";
private int numUsers = 100;
private int spawnRate = 10;
private String runTime = "5m";
private boolean webUI = false;
private boolean onlySummary = false;
private int timeoutMinutes = 10;
// Getters and setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getNumUsers() { return numUsers; }
public void setNumUsers(int numUsers) { this.numUsers = numUsers; }
public int getSpawnRate() { return spawnRate; }
public void setSpawnRate(int spawnRate) { this.spawnRate = spawnRate; }
public String getRunTime() { return runTime; }
public void setRunTime(String runTime) { this.runTime = runTime; }
public boolean isWebUI() { return webUI; }
public void setWebUI(boolean webUI) { this.webUI = webUI; }
public boolean isOnlySummary() { return onlySummary; }
public void setOnlySummary(boolean onlySummary) { this.onlySummary = onlySummary; }
public int getTimeoutMinutes() { return timeoutMinutes; }
public void setTimeoutMinutes(int timeoutMinutes) { this.timeoutMinutes = timeoutMinutes; }
}
public static class TestExecutionResult {
private String scriptName;
private boolean success;
private int exitCode;
private String output;
private String resultsDirectory;
private boolean timeout;
// Getters and setters
public String getScriptName() { return scriptName; }
public void setScriptName(String scriptName) { this.scriptName = scriptName; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public int getExitCode() { return exitCode; }
public void setExitCode(int exitCode) { this.exitCode = exitCode; }
public String getOutput() { return output; }
public void setOutput(String output) { this.output = output; }
public String getResultsDirectory() { return resultsDirectory; }
public void setResultsDirectory(String resultsDirectory) { this.resultsDirectory = resultsDirectory; }
public boolean isTimeout() { return timeout; }
public void setTimeout(boolean timeout) { this.timeout = timeout; }
}
}

3. Locust Test Generator

// LocustTestGenerator.java
package com.example.locust.generator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
@Component
public class LocustTestGenerator {
private static final Logger logger = LoggerFactory.getLogger(LocustTestGenerator.class);
@Value("${locust.scripts.directory:src/test/locust}")
private String scriptsDirectory;
public String generateLocustTestScript(LocustTestConfig config) {
StringBuilder script = new StringBuilder();
// Import statements
script.append("from locust import HttpUser, task, between, TaskSet\n");
script.append("import json\n");
script.append("import random\n\n");
// Test data
if (config.hasTestData()) {
script.append(generateTestData(config));
}
// Task sets
for (TaskSetConfig taskSet : config.getTaskSets()) {
script.append(generateTaskSet(taskSet));
}
// User classes
for (UserClassConfig userClass : config.getUserClasses()) {
script.append(generateUserClass(userClass));
}
return script.toString();
}
private String generateTestData(LocustTestConfig config) {
StringBuilder data = new StringBuilder();
data.append("# Test Data\n");
if (config.getUserCredentials() != null && !config.getUserCredentials().isEmpty()) {
data.append("USER_CREDENTIALS = [\n");
for (Map<String, String> cred : config.getUserCredentials()) {
data.append(String.format("    {'username': '%s', 'password': '%s'},\n", 
cred.get("username"), cred.get("password")));
}
data.append("]\n\n");
}
if (config.getProductIds() != null && !config.getProductIds().isEmpty()) {
data.append("PRODUCT_IDS = ").append(config.getProductIds()).append("\n\n");
}
return data.toString();
}
private String generateTaskSet(TaskSetConfig taskSet) {
StringBuilder taskSetCode = new StringBuilder();
taskSetCode.append(String.format("class %s(TaskSet):\n", taskSet.getName()));
if (taskSet.getWaitTime() != null) {
taskSetCode.append(String.format("    wait_time = %s\n\n", taskSet.getWaitTime()));
}
for (TaskConfig task : taskSet.getTasks()) {
taskSetCode.append(generateTask(task)).append("\n");
}
taskSetCode.append("\n");
return taskSetCode.toString();
}
private String generateTask(TaskConfig task) {
StringBuilder taskCode = new StringBuilder();
taskCode.append(String.format("    @task(%d)\n", task.getWeight()));
taskCode.append(String.format("    def %s(self):\n", task.getName()));
// Generate request code based on method
switch (task.getMethod().toUpperCase()) {
case "GET":
taskCode.append(generateGetRequest(task));
break;
case "POST":
taskCode.append(generatePostRequest(task));
break;
case "PUT":
taskCode.append(generatePutRequest(task));
break;
case "DELETE":
taskCode.append(generateDeleteRequest(task));
break;
}
// Add validation
if (task.getExpectedStatus() > 0) {
taskCode.append(String.format("        assert response.status_code == %d, \\\n", task.getExpectedStatus()));
taskCode.append(String.format("            f\"Expected status %d, got {response.status_code}\"\n", task.getExpectedStatus()));
}
return taskCode.toString();
}
private String generateGetRequest(TaskConfig task) {
StringBuilder request = new StringBuilder();
request.append("        with self.client.get(\"");
request.append(task.getUrl());
request.append("\"");
if (task.getHeaders() != null && !task.getHeaders().isEmpty()) {
request.append(", headers=");
request.append(convertMapToPythonDict(task.getHeaders()));
}
if (task.getName() != null) {
request.append(", name=\"");
request.append(task.getName());
request.append("\"");
}
request.append(", catch_response=True) as response:\n");
if (task.getExpectedBody() != null) {
request.append("            if \"");
request.append(task.getExpectedBody());
request.append("\" not in response.text:\n");
request.append("                response.failure(\"Expected content not found in response\")\n");
}
return request.toString();
}
private String generatePostRequest(TaskConfig task) {
StringBuilder request = new StringBuilder();
request.append("        with self.client.post(\"");
request.append(task.getUrl());
request.append("\"");
if (task.getBody() != null) {
request.append(", json=");
request.append(task.getBody());
}
if (task.getHeaders() != null && !task.getHeaders().isEmpty()) {
request.append(", headers=");
request.append(convertMapToPythonDict(task.getHeaders()));
}
if (task.getName() != null) {
request.append(", name=\"");
request.append(task.getName());
request.append("\"");
}
request.append(", catch_response=True) as response:\n");
return request.toString();
}
private String generatePutRequest(TaskConfig task) {
// Similar to POST but with PUT method
StringBuilder request = new StringBuilder();
request.append("        with self.client.put(\"");
request.append(task.getUrl());
request.append("\"");
if (task.getBody() != null) {
request.append(", json=");
request.append(task.getBody());
}
if (task.getHeaders() != null && !task.getHeaders().isEmpty()) {
request.append(", headers=");
request.append(convertMapToPythonDict(task.getHeaders()));
}
request.append(", catch_response=True) as response:\n");
return request.toString();
}
private String generateDeleteRequest(TaskConfig task) {
StringBuilder request = new StringBuilder();
request.append("        with self.client.delete(\"");
request.append(task.getUrl());
request.append("\"");
if (task.getHeaders() != null && !task.getHeaders().isEmpty()) {
request.append(", headers=");
request.append(convertMapToPythonDict(task.getHeaders()));
}
request.append(", catch_response=True) as response:\n");
return request.toString();
}
private String generateUserClass(UserClassConfig userClass) {
StringBuilder userClassCode = new StringBuilder();
userClassCode.append(String.format("class %s(HttpUser):\n", userClass.getName()));
userClassCode.append(String.format("    wait_time = %s\n", userClass.getWaitTime()));
if (userClass.getTaskSet() != null) {
userClassCode.append(String.format("    tasks = [%s]\n", userClass.getTaskSet()));
} else if (!userClass.getTasks().isEmpty()) {
userClassCode.append("    tasks = [\n");
for (String task : userClass.getTasks()) {
userClassCode.append(String.format("        %s,\n", task));
}
userClassCode.append("    ]\n");
}
if (userClass.isSetup()) {
userClassCode.append("\n    def on_start(self):\n");
userClassCode.append("        # User setup code here\n");
userClassCode.append("        self.login()\n\n");
userClassCode.append("    def login(self):\n");
userClassCode.append("        credentials = random.choice(USER_CREDENTIALS)\n");
userClassCode.append("        response = self.client.post(\"/api/auth/login\", \n");
userClassCode.append("            json=credentials)\n");
userClassCode.append("        if response.status_code == 200:\n");
userClassCode.append("            self.token = response.json().get('token')\n");
userClassCode.append("            self.client.headers = {'Authorization': f'Bearer {self.token}'}\n");
}
userClassCode.append("\n");
return userClassCode.toString();
}
private String convertMapToPythonDict(Map<String, String> map) {
if (map == null || map.isEmpty()) {
return "{}";
}
StringBuilder dict = new StringBuilder("{");
for (Map.Entry<String, String> entry : map.entrySet()) {
dict.append(String.format("'%s': '%s', ", entry.getKey(), entry.getValue()));
}
dict.append("}");
return dict.toString();
}
public void generateAndSaveTestScript(LocustTestConfig config, String filename) {
try {
String script = generateLocustTestScript(config);
Path scriptPath = Paths.get(scriptsDirectory, filename);
Files.createDirectories(scriptPath.getParent());
Files.writeString(scriptPath, script);
logger.info("Generated Locust test script: {}", scriptPath);
} catch (IOException e) {
logger.error("Failed to generate Locust test script", e);
throw new RuntimeException("Failed to generate Locust test script", e);
}
}
// Configuration classes
public static class LocustTestConfig {
private List<TaskSetConfig> taskSets = new ArrayList<>();
private List<UserClassConfig> userClasses = new ArrayList<>();
private List<Map<String, String>> userCredentials = new ArrayList<>();
private List<Integer> productIds = Arrays.asList(1, 2, 3, 4, 5);
public boolean hasTestData() {
return !userCredentials.isEmpty() || !productIds.isEmpty();
}
// Getters and setters
public List<TaskSetConfig> getTaskSets() { return taskSets; }
public List<UserClassConfig> getUserClasses() { return userClasses; }
public List<Map<String, String>> getUserCredentials() { return userCredentials; }
public List<Integer> getProductIds() { return productIds; }
}
public static class TaskSetConfig {
private String name;
private String waitTime = "between(1, 5)";
private List<TaskConfig> tasks = new ArrayList<>();
public TaskSetConfig(String name) {
this.name = name;
}
// Getters and setters
public String getName() { return name; }
public String getWaitTime() { return waitTime; }
public List<TaskConfig> getTasks() { return tasks; }
}
public static class TaskConfig {
private String name;
private int weight = 1;
private String method = "GET";
private String url;
private String body;
private Map<String, String> headers;
private int expectedStatus = 200;
private String expectedBody;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getWeight() { return weight; }
public void setWeight(int weight) { this.weight = weight; }
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public Map<String, String> getHeaders() { return headers; }
public void setHeaders(Map<String, String> headers) { this.headers = headers; }
public int getExpectedStatus() { return expectedStatus; }
public void setExpectedStatus(int expectedStatus) { this.expectedStatus = expectedStatus; }
public String getExpectedBody() { return expectedBody; }
public void setExpectedBody(String expectedBody) { this.expectedBody = expectedBody; }
}
public static class UserClassConfig {
private String name;
private String waitTime = "between(1, 5)";
private String taskSet;
private List<String> tasks = new ArrayList<>();
private boolean setup = false;
public UserClassConfig(String name) {
this.name = name;
}
// Getters and setters
public String getName() { return name; }
public String getWaitTime() { return waitTime; }
public String getTaskSet() { return taskSet; }
public void setTaskSet(String taskSet) { this.taskSet = taskSet; }
public List<String> getTasks() { return tasks; }
public boolean isSetup() { return setup; }
public void setSetup(boolean setup) { this.setup = setup; }
}
}

4. Locust4J Integration (Java-based Load Testing)

// Locust4JTestRunner.java
package com.example.locust.runner;
import io.github.zhangweihao.locus4j.client.LocustClient;
import io.github.zhangweihao.locus4j.client.LocustTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class Locust4JTestRunner {
private static final Logger logger = LoggerFactory.getLogger(Locust4JTestRunner.class);
@Value("${locust.master.host:localhost}")
private String masterHost;
@Value("${locust.master.port:8089}")
private int masterPort;
private final AtomicInteger userCount = new AtomicInteger(0);
public void runDistributedTest(List<LocustTask> tasks, int numUsers, int hatchRate) {
try {
LocustClient client = new LocustClient(masterHost, masterPort);
// Register tasks
for (LocustTask task : tasks) {
client.registerTask(task);
}
// Start the test
client.startTest(numUsers, hatchRate);
logger.info("Started distributed Locust test with {} users and hatch rate {}", 
numUsers, hatchRate);
} catch (Exception e) {
logger.error("Failed to run distributed Locust test", e);
throw new RuntimeException("Distributed Locust test failed", e);
}
}
public List<LocustTask> createApiTasks(String baseUrl) {
List<LocustTask> tasks = new ArrayList<>();
// Health check task
tasks.add(new LocustTask("health_check", 1, () -> {
try {
// Simulate HTTP request to health endpoint
Thread.sleep(100);
return true;
} catch (Exception e) {
return false;
}
}));
// User API tasks
tasks.add(new LocustTask("get_users", 2, () -> {
try {
// Simulate GET /api/users
Thread.sleep(200);
return Math.random() > 0.05; // 5% error rate
} catch (Exception e) {
return false;
}
}));
// Product API tasks
tasks.add(new LocustTask("get_products", 3, () -> {
try {
// Simulate GET /api/products
Thread.sleep(150);
return Math.random() > 0.02; // 2% error rate
} catch (Exception e) {
return false;
}
}));
// Order API tasks
tasks.add(new LocustTask("create_order", 1, () -> {
try {
// Simulate POST /api/orders
Thread.sleep(300);
return Math.random() > 0.1; // 10% error rate for orders
} catch (Exception e) {
return false;
}
}));
return tasks;
}
public CompletableFuture<Void> runBackgroundLoadTest(int durationMinutes) {
return CompletableFuture.runAsync(() -> {
try {
List<LocustTask> tasks = createApiTasks("http://localhost:8080");
runDistributedTest(tasks, 50, 5);
// Run for specified duration
Thread.sleep(durationMinutes * 60 * 1000);
// Stop the test
// Note: You would need to implement stop functionality
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Background load test interrupted");
}
});
}
}

5. Sample Locust Test Scripts

Basic API Load Test

# src/test/locust/basic_api_test.py
from locust import HttpUser, task, between
import random
import json
# Test Data
USER_CREDENTIALS = [
{"username": "[email protected]", "password": "password123"},
{"username": "[email protected]", "password": "password123"},
{"username": "[email protected]", "password": "password123"},
]
PRODUCT_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ApiUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
"""Called when a user starts the test"""
self.login()
def login(self):
credentials = random.choice(USER_CREDENTIALS)
response = self.client.post("/api/auth/login", 
json=credentials)
if response.status_code == 200:
self.token = response.json().get('token')
self.headers = {'Authorization': f'Bearer {self.token}'}
else:
self.token = None
self.headers = {}
@task(3)
def get_products(self):
with self.client.get("/api/products", 
headers=self.headers,
name="Get Products") as response:
if response.status_code != 200:
response.failure(f"Get products failed: {response.status_code}")
@task(2)
def get_product_details(self):
product_id = random.choice(PRODUCT_IDS)
with self.client.get(f"/api/products/{product_id}", 
headers=self.headers,
name="Get Product Details") as response:
if response.status_code != 200:
response.failure(f"Get product details failed: {response.status_code}")
@task(1)
def create_order(self):
order_data = {
"productId": random.choice(PRODUCT_IDS),
"quantity": random.randint(1, 5),
"notes": f"Test order from virtual user"
}
with self.client.post("/api/orders", 
json=order_data,
headers=self.headers,
name="Create Order") as response:
if response.status_code != 201:
response.failure(f"Create order failed: {response.status_code}")
@task(1)
def get_user_profile(self):
with self.client.get("/api/users/profile", 
headers=self.headers,
name="Get User Profile") as response:
if response.status_code != 200:
response.failure(f"Get user profile failed: {response.status_code}")

Advanced Scenario Test

# src/test/locust/advanced_scenario_test.py
from locust import HttpUser, task, between, TaskSet
import random
import json
class BrowseProductsTaskSet(TaskSet):
@task(3)
def view_product_list(self):
self.client.get("/api/products", name="View Product List")
@task(2)
def view_product_details(self):
product_id = random.randint(1, 100)
self.client.get(f"/api/products/{product_id}", name="View Product Details")
@task(1)
def search_products(self):
search_terms = ["laptop", "phone", "tablet", "watch", "camera"]
term = random.choice(search_terms)
self.client.get(f"/api/products/search?q={term}", name="Search Products")
@task(1)
def stop(self):
self.interrupt()
class UserAccountTaskSet(TaskSet):
def on_start(self):
self.login()
def login(self):
credentials = {
"username": f"testuser{random.randint(1, 1000)}@example.com",
"password": "testpass123"
}
response = self.client.post("/api/auth/login", json=credentials)
if response.status_code == 200:
self.token = response.json().get('token')
self.headers = {'Authorization': f'Bearer {self.token}'}
else:
self.token = None
self.headers = {}
@task(2)
def view_profile(self):
self.client.get("/api/users/profile", headers=self.headers, name="View Profile")
@task(1)
def update_profile(self):
profile_data = {
"name": f"Updated User {random.randint(1, 1000)}",
"email": f"updated{random.randint(1, 1000)}@example.com"
}
self.client.put("/api/users/profile", 
json=profile_data,
headers=self.headers,
name="Update Profile")
@task(1)
def stop(self):
self.interrupt()
class OrderTaskSet(TaskSet):
@task(2)
def view_orders(self):
self.client.get("/api/orders", headers=self.parent.headers, name="View Orders")
@task(1)
def create_order(self):
order_data = {
"items": [
{
"productId": random.randint(1, 100),
"quantity": random.randint(1, 3)
}
],
"shippingAddress": {
"street": "123 Test St",
"city": "Test City",
"zipCode": "12345"
}
}
self.client.post("/api/orders", 
json=order_data,
headers=self.parent.headers,
name="Create Order")
@task(1)
def stop(self):
self.interrupt()
class WebsiteUser(HttpUser):
wait_time = between(2, 5)
tasks = [BrowseProductsTaskSet, UserAccountTaskSet, OrderTaskSet]
def on_start(self):
self.headers = {}

6. Results Analysis and Reporting

// LocustResultsAnalyzer.java
package com.example.locust.analysis;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
@Component
public class LocustResultsAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(LocustResultsAnalyzer.class);
private final ObjectMapper objectMapper;
public LocustResultsAnalyzer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public LocustTestAnalysis analyzeCsvResults(String resultsDirectory) {
try {
Path statsPath = Paths.get(resultsDirectory + "_stats.csv");
Path failuresPath = Paths.get(resultsDirectory + "_failures.csv");
Path historyPath = Paths.get(resultsDirectory + "_stats_history.csv");
LocustTestAnalysis analysis = new LocustTestAnalysis();
analysis.setResultsDirectory(resultsDirectory);
if (Files.exists(statsPath)) {
analyzeStatsCsv(statsPath, analysis);
}
if (Files.exists(failuresPath)) {
analyzeFailuresCsv(failuresPath, analysis);
}
if (Files.exists(historyPath)) {
analyzeHistoryCsv(historyPath, analysis);
}
return analysis;
} catch (IOException e) {
logger.error("Failed to analyze Locust CSV results", e);
throw new RuntimeException("Failed to analyze Locust results", e);
}
}
public LocustTestAnalysis analyzeJsonResults(String jsonFile) {
try {
Path jsonPath = Paths.get(jsonFile);
String content = Files.readString(jsonPath);
JsonNode rootNode = objectMapper.readTree(content);
LocustTestAnalysis analysis = new LocustTestAnalysis();
analysis.setResultsDirectory(jsonFile);
extractStatsFromJson(rootNode, analysis);
extractErrorsFromJson(rootNode, analysis);
return analysis;
} catch (IOException e) {
logger.error("Failed to analyze Locust JSON results: {}", jsonFile, e);
throw new RuntimeException("Failed to analyze Locust JSON results", e);
}
}
private void analyzeStatsCsv(Path statsPath, LocustTestAnalysis analysis) throws IOException {
List<String> lines = Files.readAllLines(statsPath);
if (lines.size() < 2) return;
// Skip header
for (int i = 1; i < lines.size(); i++) {
String[] columns = lines.get(i).split(",");
if (columns.length >= 11) {
RequestStats stats = parseRequestStats(columns);
analysis.addRequestStats(stats);
}
}
}
private void analyzeFailuresCsv(Path failuresPath, LocustTestAnalysis analysis) throws IOException {
if (!Files.exists(failuresPath)) return;
List<String> lines = Files.readAllLines(failuresPath);
if (lines.size() < 2) return;
for (int i = 1; i < lines.size(); i++) {
String[] columns = lines.get(i).split(",");
if (columns.length >= 4) {
ErrorStats error = parseErrorStats(columns);
analysis.addErrorStats(error);
}
}
}
private void analyzeHistoryCsv(Path historyPath, LocustTestAnalysis analysis) throws IOException {
if (!Files.exists(historyPath)) return;
List<String> lines = Files.readAllLines(historyPath);
if (lines.size() < 2) return;
List<TimeSeriesPoint> timeSeries = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
String[] columns = lines.get(i).split(",");
if (columns.length >= 8) {
TimeSeriesPoint point = parseTimeSeriesPoint(columns);
timeSeries.add(point);
}
}
analysis.setTimeSeries(timeSeries);
}
private RequestStats parseRequestStats(String[] columns) {
RequestStats stats = new RequestStats();
stats.setName(columns[0].replace("\"", ""));
stats.setMethod(columns[1].replace("\"", ""));
stats.setNumRequests(Long.parseLong(columns[2]));
stats.setNumFailures(Long.parseLong(columns[3]));
stats.setMedianResponseTime(Integer.parseInt(columns[4]));
stats.setAverageResponseTime(Double.parseDouble(columns[5]));
stats.setMinResponseTime(Integer.parseInt(columns[6]));
stats.setMaxResponseTime(Integer.parseInt(columns[7]));
stats.setRequestsPerSecond(Double.parseDouble(columns[8]));
stats.setFailuresPerSecond(Double.parseDouble(columns[9]));
if (columns.length > 10) {
String percentileStr = columns[10].replace("\"", "").replace("%", "");
stats.setResponseTimePercentile(Double.parseDouble(percentileStr));
}
return stats;
}
private ErrorStats parseErrorStats(String[] columns) {
ErrorStats error = new ErrorStats();
error.setMethod(columns[0].replace("\"", ""));
error.setName(columns[1].replace("\"", ""));
error.setError(columns[2].replace("\"", ""));
error.setOccurrences(Integer.parseInt(columns[3]));
return error;
}
private TimeSeriesPoint parseTimeSeriesPoint(String[] columns) {
TimeSeriesPoint point = new TimeSeriesPoint();
point.setTimestamp(columns[0]);
point.setCurrentUsers(Integer.parseInt(columns[1]));
point.setRequestsPerSecond(Double.parseDouble(columns[5]));
point.setFailureRate(Double.parseDouble(columns[6]));
point.setAverageResponseTime(Double.parseDouble(columns[7]));
return point;
}
private void extractStatsFromJson(JsonNode rootNode, LocustTestAnalysis analysis) {
JsonNode stats = rootNode.path("stats");
if (stats.isArray()) {
for (JsonNode statNode : stats) {
RequestStats statsObj = new RequestStats();
statsObj.setName(statNode.path("name").asText());
statsObj.setMethod(statNode.path("method").asText());
statsObj.setNumRequests(statNode.path("num_requests").asLong());
statsObj.setNumFailures(statNode.path("num_failures").asLong());
statsObj.setAverageResponseTime(statNode.path("avg_response_time").asDouble());
statsObj.setMinResponseTime(statNode.path("min_response_time").asInt());
statsObj.setMaxResponseTime(statNode.path("max_response_time").asInt());
statsObj.setRequestsPerSecond(statNode.path("current_rps").asDouble());
analysis.addRequestStats(statsObj);
}
}
}
private void extractErrorsFromJson(JsonNode rootNode, LocustTestAnalysis analysis) {
JsonNode errors = rootNode.path("errors");
if (errors.isArray()) {
for (JsonNode errorNode : errors) {
ErrorStats error = new ErrorStats();
error.setName(errorNode.path("name").asText());
error.setMethod(errorNode.path("method").asText());
error.setError(errorNode.path("error").asText());
error.setOccurrences(errorNode.path("occurrences").asInt());
analysis.addErrorStats(error);
}
}
}
public PerformanceReport generateReport(LocustTestAnalysis analysis) {
PerformanceReport report = new PerformanceReport();
report.setAnalysis(analysis);
report.setGeneratedAt(new Date());
// Calculate overall metrics
calculateOverallMetrics(analysis, report);
// Generate recommendations
generateRecommendations(analysis, report);
// Determine status
determineStatus(analysis, report);
return report;
}
private void calculateOverallMetrics(LocustTestAnalysis analysis, PerformanceReport report) {
long totalRequests = analysis.getRequestStats().stream()
.mapToLong(RequestStats::getNumRequests)
.sum();
long totalFailures = analysis.getRequestStats().stream()
.mapToLong(RequestStats::getNumFailures)
.sum();
double failureRate = totalRequests > 0 ? (double) totalFailures / totalRequests : 0.0;
double successRate = 1.0 - failureRate;
double avgResponseTime = analysis.getRequestStats().stream()
.mapToDouble(RequestStats::getAverageResponseTime)
.average()
.orElse(0.0);
report.setTotalRequests(totalRequests);
report.setTotalFailures(totalFailures);
report.setFailureRate(failureRate);
report.setSuccessRate(successRate);
report.setAverageResponseTime(avgResponseTime);
}
private void generateRecommendations(LocustTestAnalysis analysis, PerformanceReport report) {
List<String> recommendations = new ArrayList<>();
// Check for high failure rates
if (report.getFailureRate() > 0.05) {
recommendations.add("Investigate and fix the root cause of high failure rates");
}
// Check for slow endpoints
analysis.getRequestStats().stream()
.filter(stats -> stats.getAverageResponseTime() > 1000)
.forEach(stats -> {
recommendations.add(String.format(
"Optimize endpoint %s %s (avg response time: %.2f ms)",
stats.getMethod(), stats.getName(), stats.getAverageResponseTime()));
});
// Check for high error rates on specific endpoints
analysis.getRequestStats().stream()
.filter(stats -> stats.getNumRequests() > 0)
.filter(stats -> (double) stats.getNumFailures() / stats.getNumRequests() > 0.1)
.forEach(stats -> {
recommendations.add(String.format(
"Fix errors in endpoint %s %s (failure rate: %.1f%%)",
stats.getMethod(), stats.getName(),
(double) stats.getNumFailures() / stats.getNumRequests() * 100));
});
report.setRecommendations(recommendations);
}
private void determineStatus(LocustTestAnalysis analysis, PerformanceReport report) {
if (report.getFailureRate() > 0.1) {
report.setStatus(PerformanceReport.ReportStatus.FAILED);
} else if (report.getFailureRate() > 0.05 || report.getAverageResponseTime() > 2000) {
report.setStatus(PerformanceReport.ReportStatus.WARNING);
} else {
report.setStatus(PerformanceReport.ReportStatus.PASSED);
}
}
// Data classes
public static class LocustTestAnalysis {
private String resultsDirectory;
private List<RequestStats> requestStats = new ArrayList<>();
private List<ErrorStats> errorStats = new ArrayList<>();
private List<TimeSeriesPoint> timeSeries = new ArrayList<>();
// Getters and setters
public String getResultsDirectory() { return resultsDirectory; }
public void setResultsDirectory(String resultsDirectory) { this.resultsDirectory = resultsDirectory; }
public List<RequestStats> getRequestStats() { return requestStats; }
public void setRequestStats(List<RequestStats> requestStats) { this.requestStats = requestStats; }
public List<ErrorStats> getErrorStats() { return errorStats; }
public void setErrorStats(List<ErrorStats> errorStats) { this.errorStats = errorStats; }
public List<TimeSeriesPoint> getTimeSeries() { return timeSeries; }
public void setTimeSeries(List<TimeSeriesPoint> timeSeries) { this.timeSeries = timeSeries; }
public void addRequestStats(RequestStats stats) { this.requestStats.add(stats); }
public void addErrorStats(ErrorStats error) { this.errorStats.add(error); }
}
public static class RequestStats {
private String name;
private String method;
private long numRequests;
private long numFailures;
private int medianResponseTime;
private double averageResponseTime;
private int minResponseTime;
private int maxResponseTime;
private double requestsPerSecond;
private double failuresPerSecond;
private double responseTimePercentile;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public long getNumRequests() { return numRequests; }
public void setNumRequests(long numRequests) { this.numRequests = numRequests; }
public long getNumFailures() { return numFailures; }
public void setNumFailures(long numFailures) { this.numFailures = numFailures; }
public int getMedianResponseTime() { return medianResponseTime; }
public void setMedianResponseTime(int medianResponseTime) { this.medianResponseTime = medianResponseTime; }
public double getAverageResponseTime() { return averageResponseTime; }
public void setAverageResponseTime(double averageResponseTime) { this.averageResponseTime = averageResponseTime; }
public int getMinResponseTime() { return minResponseTime; }
public void setMinResponseTime(int minResponseTime) { this.minResponseTime = minResponseTime; }
public int getMaxResponseTime() { return maxResponseTime; }
public void setMaxResponseTime(int maxResponseTime) { this.maxResponseTime = maxResponseTime; }
public double getRequestsPerSecond() { return requestsPerSecond; }
public void setRequestsPerSecond(double requestsPerSecond) { this.requestsPerSecond = requestsPerSecond; }
public double getFailuresPerSecond() { return failuresPerSecond; }
public void setFailuresPerSecond(double failuresPerSecond) { this.failuresPerSecond = failuresPerSecond; }
public double getResponseTimePercentile() { return responseTimePercentile; }
public void setResponseTimePercentile(double responseTimePercentile) { this.responseTimePercentile = responseTimePercentile; }
}
public static class ErrorStats {
private String method;
private String name;
private String error;
private int occurrences;
// Getters and setters
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public int getOccurrences() { return occurrences; }
public void setOccurrences(int occurrences) { this.occurrences = occurrences; }
}
public static class TimeSeriesPoint {
private String timestamp;
private int currentUsers;
private double requestsPerSecond;
private double failureRate;
private double averageResponseTime;
// Getters and setters
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
public int getCurrentUsers() { return currentUsers; }
public void setCurrentUsers(int currentUsers) { this.currentUsers = currentUsers; }
public double getRequestsPerSecond() { return requestsPerSecond; }
public void setRequestsPerSecond(double requestsPerSecond) { this.requestsPerSecond = requestsPerSecond; }
public double getFailureRate() { return failureRate; }
public void setFailureRate(double failureRate) { this.failureRate = failureRate; }
public double getAverageResponseTime() { return averageResponseTime; }
public void setAverageResponseTime(double averageResponseTime) { this.averageResponseTime = averageResponseTime; }
}
public static class PerformanceReport {
public enum ReportStatus { PASSED, WARNING, FAILED }
private LocustTestAnalysis analysis;
private Date generatedAt;
private ReportStatus status;
private long totalRequests;
private long totalFailures;
private double failureRate;
private double successRate;
private double averageResponseTime;
private List<String> recommendations = new ArrayList<>();
// Getters and setters
public LocustTestAnalysis getAnalysis() { return analysis; }
public void setAnalysis(LocustTestAnalysis analysis) { this.analysis = analysis; }
public Date getGeneratedAt() { return generatedAt; }
public void setGeneratedAt(Date generatedAt) { this.generatedAt = generatedAt; }
public ReportStatus getStatus() { return status; }
public void setStatus(ReportStatus status) { this.status = status; }
public long getTotalRequests() { return totalRequests; }
public void setTotalRequests(long totalRequests) { this.totalRequests = totalRequests; }
public long getTotalFailures() { return totalFailures; }
public void setTotalFailures(long totalFailures) { this.totalFailures = totalFailures; }
public double getFailureRate() { return failureRate; }
public void setFailureRate(double failureRate) { this.failureRate = failureRate; }
public double getSuccessRate() { return successRate; }
public void setSuccessRate(double successRate) { this.successRate = successRate; }
public double getAverageResponseTime() { return averageResponseTime; }
public void setAverageResponseTime(double averageResponseTime) { this.averageResponseTime = averageResponseTime; }
public List<String> getRecommendations() { return recommendations; }
public void setRecommendations(List<String> recommendations) { this.recommendations = recommendations; }
}
}

7. JUnit 5 Test Integration

// LocustPerformanceTest.java
package com.example.locust.test;
import com.example.locust.generator.LocustTestGenerator;
import com.example.locust.runner.LocustTestRunner;
import com.example.locust.analysis.LocustResultsAnalyzer;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class LocustPerformanceTest {
@Container
static GenericContainer<?> locustContainer = new GenericContainer<>("locustio/locust")
.withExposedPorts(8089)
.withCommand("-f /locust-scripts/basic_api_test.py --host http://host.docker.internal:8080");
@Autowired
private LocustTestGenerator testGenerator;
@Autowired
private LocustTestRunner testRunner;
@Autowired
private LocustResultsAnalyzer resultsAnalyzer;
@BeforeAll
void setup() {
generateTestScripts();
}
@Test
@DisplayName("Basic API Load Test - 100 Users")
void testBasicApiLoad() {
LocustTestRunner.LocustOptions options = new LocustTestRunner.LocustOptions();
options.setNumUsers(100);
options.setSpawnRate(10);
options.setRunTime("2m");
options.setOnlySummary(true);
LocustTestRunner.TestExecutionResult result = testRunner.runLocustTest(
"basic_api_test.py", options);
assertTrue(result.isSuccess(), "Locust test should complete successfully");
assertFalse(result.isTimeout(), "Locust test should not timeout");
// Analyze results
LocustResultsAnalyzer.LocustTestAnalysis analysis = 
resultsAnalyzer.analyzeCsvResults(result.getResultsDirectory());
LocustResultsAnalyzer.PerformanceReport report = 
resultsAnalyzer.generateReport(analysis);
assertEquals(LocustResultsAnalyzer.PerformanceReport.ReportStatus.PASSED, 
report.getStatus(), "Performance test should pass");
assertTrue(report.getSuccessRate() >= 0.95,
"Success rate should be at least 95%");
}
@Test
@DisplayName("Advanced Scenario Test")
void testAdvancedScenario() {
LocustTestRunner.TestExecutionResult result = testRunner.runLocustTest(
"advanced_scenario_test.py", 
new LocustTestRunner.LocustOptions());
assertTrue(result.isSuccess(), "Advanced scenario test should complete successfully");
LocustResultsAnalyzer.LocustTestAnalysis analysis = 
resultsAnalyzer.analyzeCsvResults(result.getResultsDirectory());
LocustResultsAnalyzer.PerformanceReport report = 
resultsAnalyzer.generateReport(analysis);
System.out.println("Performance Report:");
System.out.println("Total Requests: " + report.getTotalRequests());
System.out.println("Success Rate: " + String.format("%.2f%%", report.getSuccessRate() * 100));
System.out.println("Average Response Time: " + report.getAverageResponseTime() + "ms");
System.out.println("Recommendations: " + report.getRecommendations());
assertTrue(report.getSuccessRate() >= 0.90,
"Advanced scenario should meet minimum success rate");
}
@Test
@DisplayName("Docker-based Load Test")
void testDockerLoadTest() {
LocustTestRunner.LocustOptions options = new LocustTestRunner.LocustOptions();
options.setNumUsers(50);
options.setSpawnRate(5);
options.setRunTime("1m");
LocustTestRunner.TestExecutionResult result = testRunner.runLocustWithDocker(
"basic_api_test.py", options);
assertTrue(result.isSuccess(), "Docker-based test should complete successfully");
}
private void generateTestScripts() {
// Generate a programmatic test script
LocustTestGenerator.LocustTestConfig config = new LocustTestGenerator.LocustTestConfig();
// Add user credentials
for (int i = 1; i <= 10; i++) {
Map<String, String> creds = new HashMap<>();
creds.put("username", "testuser" + i + "@example.com");
creds.put("password", "password123");
config.getUserCredentials().add(creds);
}
// Create API task set
LocustTestGenerator.TaskSetConfig apiTasks = new LocustTestGenerator.TaskSetConfig("ApiTaskSet");
LocustTestGenerator.TaskConfig getProducts = new LocustTestGenerator.TaskConfig();
getProducts.setName("get_products");
getProducts.setWeight(3);
getProducts.setMethod("GET");
getProducts.setUrl("/api/products");
getProducts.setExpectedStatus(200);
apiTasks.getTasks().add(getProducts);
LocustTestGenerator.TaskConfig getUserProfile = new LocustTestGenerator.TaskConfig();
getUserProfile.setName("get_user_profile");
getUserProfile.setWeight(2);
getUserProfile.setMethod("GET");
getUserProfile.setUrl("/api/users/profile");
getUserProfile.setExpectedStatus(200);
Map<String, String> authHeaders = new HashMap<>();
authHeaders.put("Authorization", "Bearer ${token}");
getUserProfile.setHeaders(authHeaders);
apiTasks.getTasks().add(getUserProfile);
config.getTaskSets().add(apiTasks);
// Create user class
LocustTestGenerator.UserClassConfig apiUser = new LocustTestGenerator.UserClassConfig("ApiUser");
apiUser.setTaskSet("ApiTaskSet");
apiUser.setSetup(true);
config.getUserClasses().add(apiUser);
testGenerator.generateAndSaveTestScript(config, "generated_api_test.py");
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("locust.scripts.directory", () -> "src/test/locust");
registry.add("locust.results.directory", () -> "target/locust-results");
}
}

This comprehensive Locust integration provides:

  • Python script generation from Java configuration
  • Multiple execution modes (local, Docker, distributed)
  • Results analysis and performance reporting
  • Custom task definitions for complex scenarios
  • JUnit 5 integration for CI/CD pipelines
  • Real-time metrics collection and analysis
  • Distributed testing capabilities with Locust4J
  • Comprehensive reporting with recommendations

The setup enables robust performance testing of Java applications using Locust's powerful load testing capabilities with the convenience of Java configuration and analysis.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper