Performance Testing with k6 in Java

Comprehensive k6 Integration Guide for Java Applications

1. k6 Test Configuration and Setup

Maven Dependencies and Plugin Configuration

<!-- pom.xml -->
<properties>
<k6.version>0.0.9</k6.version>
<gatling.version>3.9.5</gatling.version>
<jmeter.version>5.6.2</jmeter.version>
</properties>
<dependencies>
<!-- k6 Java API -->
<dependency>
<groupId>io.github.k6</groupId>
<artifactId>k6-java-api</artifactId>
<version>${k6.version}</version>
<scope>test</scope>
</dependency>
<!-- Test Containers for k6 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Metrics Collection -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.11.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- k6 Maven Plugin -->
<plugin>
<groupId>io.github.k6</groupId>
<artifactId>k6-maven-plugin</artifactId>
<version>${k6.version}</version>
<configuration>
<testFile>src/test/k6/performance-test.js</testFile>
<output>results/performance-test.json</output>
<quiet>false</quiet>
</configuration>
<executions>
<execution>
<id>performance-test</id>
<phase>verify</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

2. k6 Test Script Generator

// K6TestGenerator.java
package com.example.k6.generator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
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 K6TestGenerator {
private static final Logger logger = LoggerFactory.getLogger(K6TestGenerator.class);
private final ObjectMapper objectMapper;
private final ResourceLoader resourceLoader;
@Value("${k6.scripts.directory:src/test/k6}")
private String scriptsDirectory;
public K6TestGenerator(ObjectMapper objectMapper, ResourceLoader resourceLoader) {
this.objectMapper = objectMapper;
this.resourceLoader = resourceLoader;
}
public String generateLoadTestScript(LoadTestConfig config) {
StringBuilder script = new StringBuilder();
// Import statements
script.append("import http from 'k6/http';\n");
script.append("import { check, sleep } from 'k6';\n");
script.append("import { Trend, Counter, Rate } from 'k6/metrics';\n\n");
// Custom metrics
script.append("// Custom Metrics\n");
script.append("const responseTimeTrend = new Trend('response_time');\n");
script.append("const successRate = new Rate('success_rate');\n");
script.append("const errorCounter = new Counter('errors');\n\n");
// Test configuration
script.append("// Test Configuration\n");
script.append("export const options = {\n");
script.append(generateOptions(config));
script.append("};\n\n");
// Test scenarios
script.append("// Test Scenarios\n");
script.append(generateScenarios(config));
// Setup function (if needed)
if (config.hasSetup()) {
script.append(generateSetupFunction(config));
}
// Main test function
script.append(generateMainTestFunction(config));
// Teardown function (if needed)
if (config.hasTeardown()) {
script.append(generateTeardownFunction(config));
}
return script.toString();
}
private String generateOptions(LoadTestConfig config) {
StringBuilder options = new StringBuilder();
options.append("  stages: [\n");
for (LoadTestConfig.Stage stage : config.getStages()) {
options.append(String.format("    { duration: '%s', target: %d },\n", 
stage.getDuration(), stage.getTarget()));
}
options.append("  ],\n");
options.append(String.format("  thresholds: {\n"));
options.append(String.format("    'http_req_duration': ['p(95)<%d'],\n", config.getThresholds().getResponseTime95p()));
options.append(String.format("    'http_req_failed': ['rate<%.2f'],\n", config.getThresholds().getErrorRate()));
options.append(String.format("    'success_rate': ['rate>%.2f'],\n", config.getThresholds().getSuccessRate()));
options.append("  },\n");
return options.toString();
}
private String generateScenarios(LoadTestConfig config) {
StringBuilder scenarios = new StringBuilder();
for (LoadTestConfig.Scenario scenario : config.getScenarios()) {
scenarios.append(String.format("// Scenario: %s\n", scenario.getName()));
scenarios.append(String.format("function %s() {\n", scenario.getFunctionName()));
scenarios.append(generateScenarioSteps(scenario));
scenarios.append("}\n\n");
}
return scenarios.toString();
}
private String generateScenarioSteps(LoadTestConfig.Scenario scenario) {
StringBuilder steps = new StringBuilder();
for (LoadTestConfig.Step step : scenario.getSteps()) {
switch (step.getType()) {
case "http":
steps.append(generateHttpStep(step));
break;
case "sleep":
steps.append(String.format("  sleep(%d);\n", step.getDuration()));
break;
case "check":
steps.append(generateCheckStep(step));
break;
}
}
return steps.toString();
}
private String generateHttpStep(LoadTestConfig.Step step) {
StringBuilder httpStep = new StringBuilder();
httpStep.append(String.format("  const response = http.%s('%s', ", 
step.getMethod().toLowerCase(), step.getUrl()));
if (step.getBody() != null) {
httpStep.append(step.getBody());
}
if (step.getParams() != null && !step.getParams().isEmpty()) {
httpStep.append(", { headers: { ");
step.getParams().forEach((key, value) -> {
httpStep.append(String.format("'%s': '%s', ", key, value));
});
httpStep.append("} }");
}
httpStep.append(");\n\n");
// Add checks
httpStep.append("  check(response, {\n");
httpStep.append(String.format("    'status is %d': (r) => r.status === %d,\n", 
step.getExpectedStatus(), step.getExpectedStatus()));
if (step.getExpectedBody() != null) {
httpStep.append(String.format("    'response body contains expected data': (r) => r.body.includes('%s'),\n", 
step.getExpectedBody()));
}
httpStep.append("  });\n\n");
// Record metrics
httpStep.append("  responseTimeTrend.add(response.timings.duration);\n");
httpStep.append(String.format("  successRate.add(response.status === %d);\n", step.getExpectedStatus()));
httpStep.append("  if (response.status !== 200) {\n");
httpStep.append("    errorCounter.add(1);\n");
httpStep.append("  }\n\n");
return httpStep.toString();
}
private String generateCheckStep(LoadTestConfig.Step step) {
return String.format("  check(null, { '%s': () => true });\n", step.getName());
}
private String generateMainTestFunction(LoadTestConfig config) {
StringBuilder mainFunction = new StringBuilder();
mainFunction.append("// Main Test Function\n");
mainFunction.append("export default function() {\n");
for (LoadTestConfig.Scenario scenario : config.getScenarios()) {
if (scenario.getWeight() > 0) {
mainFunction.append(String.format("  %s();\n", scenario.getFunctionName()));
}
}
mainFunction.append("}\n");
return mainFunction.toString();
}
private String generateSetupFunction(LoadTestConfig config) {
return String.format("""
// Setup Function
export function setup() {
// Setup code here
return { token: 'test-token' };
}
""");
}
private String generateTeardownFunction(LoadTestConfig config) {
return """
// Teardown Function
export function teardown(data) {
// Cleanup code here
}
""";
}
public void generateAndSaveTestScript(LoadTestConfig config, String filename) {
try {
String script = generateLoadTestScript(config);
Path scriptPath = Paths.get(scriptsDirectory, filename);
Files.createDirectories(scriptPath.getParent());
Files.writeString(scriptPath, script);
logger.info("Generated k6 test script: {}", scriptPath);
} catch (IOException e) {
logger.error("Failed to generate k6 test script", e);
throw new RuntimeException("Failed to generate k6 test script", e);
}
}
public static class LoadTestConfig {
private List<Stage> stages = new ArrayList<>();
private List<Scenario> scenarios = new ArrayList<>();
private Thresholds thresholds = new Thresholds();
private boolean setup = false;
private boolean teardown = false;
// Getters and setters
public List<Stage> getStages() { return stages; }
public List<Scenario> getScenarios() { return scenarios; }
public Thresholds getThresholds() { return thresholds; }
public boolean hasSetup() { return setup; }
public boolean hasTeardown() { return teardown; }
public static class Stage {
private String duration;
private int target;
public Stage(String duration, int target) {
this.duration = duration;
this.target = target;
}
// Getters
public String getDuration() { return duration; }
public int getTarget() { return target; }
}
public static class Scenario {
private String name;
private String functionName;
private int weight = 1;
private List<Step> steps = new ArrayList<>();
public Scenario(String name) {
this.name = name;
this.functionName = name.toLowerCase().replace(" ", "_");
}
// Getters and setters
public String getName() { return name; }
public String getFunctionName() { return functionName; }
public int getWeight() { return weight; }
public List<Step> getSteps() { return steps; }
}
public static class Step {
private String type; // http, sleep, check
private String name;
private String method;
private String url;
private String body;
private Map<String, String> params;
private int expectedStatus = 200;
private String expectedBody;
private int duration; // for sleep steps
// Getters and setters
public String getType() { return type; }
public String getName() { return name; }
public String getMethod() { return method; }
public String getUrl() { return url; }
public String getBody() { return body; }
public Map<String, String> getParams() { return params; }
public int getExpectedStatus() { return expectedStatus; }
public String getExpectedBody() { return expectedBody; }
public int getDuration() { return duration; }
}
public static class Thresholds {
private int responseTime95p = 500;
private double errorRate = 0.01;
private double successRate = 0.95;
// Getters
public int getResponseTime95p() { return responseTime95p; }
public double getErrorRate() { return errorRate; }
public double getSuccessRate() { return successRate; }
}
}
}

3. k6 Test Runner Service

// K6TestRunner.java
package com.example.k6.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.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
@Service
public class K6TestRunner {
private static final Logger logger = Logger.getLogger(K6TestRunner.class);
@Value("${k6.binary.path:k6}")
private String k6BinaryPath;
@Value("${k6.scripts.directory:src/test/k6}")
private String scriptsDirectory;
@Value("${k6.results.directory:target/k6-results}")
private String resultsDirectory;
private final ExecutorService executorService = Executors.newCachedThreadPool();
public TestExecutionResult runTest(String testScript, TestOptions options) {
try {
Path scriptPath = Paths.get(scriptsDirectory, testScript);
Path outputPath = Paths.get(resultsDirectory, 
testScript.replace(".js", "-" + System.currentTimeMillis() + ".json"));
Files.createDirectories(outputPath.getParent());
List<String> command = buildK6Command(scriptPath, outputPath, options);
logger.info("Executing k6 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());
int exitCode = process.waitFor();
TestExecutionResult result = new TestExecutionResult();
result.setExitCode(exitCode);
result.setOutput(output);
result.setOutputFile(outputPath.toString());
result.setSuccess(exitCode == 0);
if (exitCode == 0) {
logger.info("k6 test completed successfully: {}", testScript);
} else {
logger.error("k6 test failed with exit code {}: {}", exitCode, testScript);
}
return result;
} catch (IOException | InterruptedException e) {
logger.error("Failed to execute k6 test: {}", testScript, e);
throw new RuntimeException("k6 test execution failed", e);
}
}
public CompletableFuture<TestExecutionResult> runTestAsync(String testScript, TestOptions options) {
return CompletableFuture.supplyAsync(() -> runTest(testScript, options), executorService);
}
public TestExecutionResult runTestWithDocker(String testScript, TestOptions options) {
try {
List<String> command = new ArrayList<>();
command.add("docker");
command.add("run");
command.add("--rm");
command.add("-i");
command.add("-v");
command.add(Paths.get(scriptsDirectory).toAbsolutePath() + ":/scripts");
command.add("-v");
command.add(Paths.get(resultsDirectory).toAbsolutePath() + ":/results");
command.add("grafana/k6:latest");
command.add("run");
command.add("--out");
command.add("json=/results/" + testScript.replace(".js", "-" + System.currentTimeMillis() + ".json"));
command.add("/scripts/" + testScript);
// Add k6 options
if (options.getVus() > 0) {
command.add("--vus");
command.add(String.valueOf(options.getVus()));
}
if (options.getDuration() != null) {
command.add("--duration");
command.add(options.getDuration());
}
logger.info("Executing k6 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());
int exitCode = process.waitFor();
TestExecutionResult result = new TestExecutionResult();
result.setExitCode(exitCode);
result.setOutput(output);
result.setSuccess(exitCode == 0);
return result;
} catch (IOException | InterruptedException e) {
logger.error("Failed to execute k6 test with Docker: {}", testScript, e);
throw new RuntimeException("k6 Docker test execution failed", e);
}
}
private List<String> buildK6Command(Path scriptPath, Path outputPath, TestOptions options) {
List<String> command = new ArrayList<>();
command.add(k6BinaryPath);
command.add("run");
command.add("--out");
command.add("json=" + outputPath.toString());
// Add k6 options
if (options.getVus() > 0) {
command.add("--vus");
command.add(String.valueOf(options.getVus()));
}
if (options.getIterations() > 0) {
command.add("--iterations");
command.add(String.valueOf(options.getIterations()));
}
if (options.getDuration() != null) {
command.add("--duration");
command.add(options.getDuration());
}
if (options.getStages() != null && !options.getStages().isEmpty()) {
command.add("--stage");
command.add(String.join(",", options.getStages()));
}
command.add(scriptPath.toString());
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");
}
}
return output.toString();
}
public static class TestOptions {
private int vus;
private int iterations;
private String duration;
private List<String> stages = new ArrayList<>();
// Getters and setters
public int getVus() { return vus; }
public void setVus(int vus) { this.vus = vus; }
public int getIterations() { return iterations; }
public void setIterations(int iterations) { this.iterations = iterations; }
public String getDuration() { return duration; }
public void setDuration(String duration) { this.duration = duration; }
public List<String> getStages() { return stages; }
public void setStages(List<String> stages) { this.stages = stages; }
}
public static class TestExecutionResult {
private boolean success;
private int exitCode;
private String output;
private String outputFile;
private long executionTime;
// Getters and setters
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 getOutputFile() { return outputFile; }
public void setOutputFile(String outputFile) { this.outputFile = outputFile; }
public long getExecutionTime() { return executionTime; }
public void setExecutionTime(long executionTime) { this.executionTime = executionTime; }
}
}

4. Performance Test Results Analyzer

// K6ResultsAnalyzer.java
package com.example.k6.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 K6ResultsAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(K6ResultsAnalyzer.class);
private final ObjectMapper objectMapper;
public K6ResultsAnalyzer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public TestAnalysis analyzeResults(String resultsFile) {
try {
Path filePath = Paths.get(resultsFile);
String content = Files.readString(filePath);
JsonNode rootNode = objectMapper.readTree(content);
TestAnalysis analysis = new TestAnalysis();
analysis.setTestName(filePath.getFileName().toString());
analysis.setTimestamp(System.currentTimeMillis());
extractMetrics(rootNode, analysis);
extractThresholds(rootNode, analysis);
extractCustomMetrics(rootNode, analysis);
return analysis;
} catch (IOException e) {
logger.error("Failed to analyze k6 results: {}", resultsFile, e);
throw new RuntimeException("Failed to analyze k6 results", e);
}
}
private void extractMetrics(JsonNode rootNode, TestAnalysis analysis) {
JsonNode metrics = rootNode.path("metrics");
// HTTP metrics
analysis.setHttpRequests(extractMetricValue(metrics, "http_reqs", "count"));
analysis.setFailedRequests(extractMetricValue(metrics, "http_req_failed", "rate"));
analysis.setTotalDuration(extractMetricValue(metrics, "iteration_duration", "avg"));
// Response times
analysis.setResponseTimeAvg(extractMetricValue(metrics, "http_req_duration", "avg"));
analysis.setResponseTimeP95(extractMetricValue(metrics, "http_req_duration", "p(95)"));
analysis.setResponseTimeP99(extractMetricValue(metrics, "http_req_duration", "p(99)"));
// Throughput
analysis.setRequestsPerSecond(extractMetricValue(metrics, "http_reqs", "rate"));
// Data transferred
analysis.setDataSent(extractMetricValue(metrics, "data_sent", "count"));
analysis.setDataReceived(extractMetricValue(metrics, "data_received", "count"));
}
private void extractThresholds(JsonNode rootNode, TestAnalysis analysis) {
JsonNode thresholds = rootNode.path("thresholds");
List<ThresholdResult> thresholdResults = new ArrayList<>();
if (thresholds.isObject()) {
thresholds.fields().forEachRemaining(entry -> {
String thresholdName = entry.getKey();
JsonNode thresholdData = entry.getValue();
ThresholdResult result = new ThresholdResult();
result.setName(thresholdName);
result.setOk(thresholdData.path("ok").asBoolean());
result.setActualValue(thresholdData.path("values").path("rate").asDouble());
thresholdResults.add(result);
});
}
analysis.setThresholdResults(thresholdResults);
}
private void extractCustomMetrics(JsonNode rootNode, TestAnalysis analysis) {
JsonNode metrics = rootNode.path("metrics");
Map<String, Object> customMetrics = new HashMap<>();
// Extract custom metrics
if (metrics.has("response_time")) {
customMetrics.put("response_time_avg", extractMetricValue(metrics, "response_time", "avg"));
customMetrics.put("response_time_max", extractMetricValue(metrics, "response_time", "max"));
}
if (metrics.has("success_rate")) {
customMetrics.put("success_rate", extractMetricValue(metrics, "success_rate", "rate"));
}
if (metrics.has("errors")) {
customMetrics.put("total_errors", extractMetricValue(metrics, "errors", "count"));
}
analysis.setCustomMetrics(customMetrics);
}
private double extractMetricValue(JsonNode metrics, String metricName, String valueType) {
JsonNode metric = metrics.path(metricName);
if (metric.isMissingNode()) {
return 0.0;
}
JsonNode values = metric.path("values");
if (values.has(valueType)) {
return values.path(valueType).asDouble();
}
return 0.0;
}
public PerformanceReport generateReport(TestAnalysis analysis) {
PerformanceReport report = new PerformanceReport();
report.setAnalysis(analysis);
report.setGeneratedAt(new Date());
// Calculate performance score
double performanceScore = calculatePerformanceScore(analysis);
report.setPerformanceScore(performanceScore);
// Generate recommendations
List<String> recommendations = generateRecommendations(analysis);
report.setRecommendations(recommendations);
// Determine overall status
report.setStatus(determineOverallStatus(analysis));
return report;
}
private double calculatePerformanceScore(TestAnalysis analysis) {
double score = 100.0;
// Deduct points for high response times
if (analysis.getResponseTimeP95() > 1000) {
score -= 20;
} else if (analysis.getResponseTimeP95() > 500) {
score -= 10;
}
// Deduct points for high error rate
double errorRate = analysis.getFailedRequests();
if (errorRate > 0.05) {
score -= 30;
} else if (errorRate > 0.01) {
score -= 15;
}
// Deduct points for failed thresholds
long failedThresholds = analysis.getThresholdResults().stream()
.filter(result -> !result.isOk())
.count();
score -= failedThresholds * 10;
return Math.max(0, score);
}
private List<String> generateRecommendations(TestAnalysis analysis) {
List<String> recommendations = new ArrayList<>();
if (analysis.getResponseTimeP95() > 1000) {
recommendations.add("Optimize backend services to reduce response times above 1 second");
}
if (analysis.getFailedRequests() > 0.05) {
recommendations.add("Investigate and fix the root cause of high error rates");
}
if (analysis.getRequestsPerSecond() < 10) {
recommendations.add("Consider performance optimizations to increase throughput");
}
// Check threshold failures
analysis.getThresholdResults().stream()
.filter(result -> !result.isOk())
.forEach(result -> {
recommendations.add(String.format("Threshold failed: %s (actual: %.2f)", 
result.getName(), result.getActualValue()));
});
return recommendations;
}
private PerformanceReport.ReportStatus determineOverallStatus(TestAnalysis analysis) {
long failedThresholds = analysis.getThresholdResults().stream()
.filter(result -> !result.isOk())
.count();
if (failedThresholds > 0) {
return PerformanceReport.ReportStatus.FAILED;
} else if (analysis.getFailedRequests() > 0.01) {
return PerformanceReport.ReportStatus.WARNING;
} else {
return PerformanceReport.ReportStatus.PASSED;
}
}
public static class TestAnalysis {
private String testName;
private long timestamp;
private long httpRequests;
private double failedRequests;
private double totalDuration;
private double responseTimeAvg;
private double responseTimeP95;
private double responseTimeP99;
private double requestsPerSecond;
private long dataSent;
private long dataReceived;
private List<ThresholdResult> thresholdResults = new ArrayList<>();
private Map<String, Object> customMetrics = new HashMap<>();
// Getters and setters
public String getTestName() { return testName; }
public void setTestName(String testName) { this.testName = testName; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public long getHttpRequests() { return httpRequests; }
public void setHttpRequests(long httpRequests) { this.httpRequests = httpRequests; }
public double getFailedRequests() { return failedRequests; }
public void setFailedRequests(double failedRequests) { this.failedRequests = failedRequests; }
public double getTotalDuration() { return totalDuration; }
public void setTotalDuration(double totalDuration) { this.totalDuration = totalDuration; }
public double getResponseTimeAvg() { return responseTimeAvg; }
public void setResponseTimeAvg(double responseTimeAvg) { this.responseTimeAvg = responseTimeAvg; }
public double getResponseTimeP95() { return responseTimeP95; }
public void setResponseTimeP95(double responseTimeP95) { this.responseTimeP95 = responseTimeP95; }
public double getResponseTimeP99() { return responseTimeP99; }
public void setResponseTimeP99(double responseTimeP99) { this.responseTimeP99 = responseTimeP99; }
public double getRequestsPerSecond() { return requestsPerSecond; }
public void setRequestsPerSecond(double requestsPerSecond) { this.requestsPerSecond = requestsPerSecond; }
public long getDataSent() { return dataSent; }
public void setDataSent(long dataSent) { this.dataSent = dataSent; }
public long getDataReceived() { return dataReceived; }
public void setDataReceived(long dataReceived) { this.dataReceived = dataReceived; }
public List<ThresholdResult> getThresholdResults() { return thresholdResults; }
public void setThresholdResults(List<ThresholdResult> thresholdResults) { this.thresholdResults = thresholdResults; }
public Map<String, Object> getCustomMetrics() { return customMetrics; }
public void setCustomMetrics(Map<String, Object> customMetrics) { this.customMetrics = customMetrics; }
}
public static class ThresholdResult {
private String name;
private boolean ok;
private double actualValue;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public boolean isOk() { return ok; }
public void setOk(boolean ok) { this.ok = ok; }
public double getActualValue() { return actualValue; }
public void setActualValue(double actualValue) { this.actualValue = actualValue; }
}
public static class PerformanceReport {
public enum ReportStatus { PASSED, WARNING, FAILED }
private TestAnalysis analysis;
private Date generatedAt;
private double performanceScore;
private ReportStatus status;
private List<String> recommendations = new ArrayList<>();
// Getters and setters
public TestAnalysis getAnalysis() { return analysis; }
public void setAnalysis(TestAnalysis analysis) { this.analysis = analysis; }
public Date getGeneratedAt() { return generatedAt; }
public void setGeneratedAt(Date generatedAt) { this.generatedAt = generatedAt; }
public double getPerformanceScore() { return performanceScore; }
public void setPerformanceScore(double performanceScore) { this.performanceScore = performanceScore; }
public ReportStatus getStatus() { return status; }
public void setStatus(ReportStatus status) { this.status = status; }
public List<String> getRecommendations() { return recommendations; }
public void setRecommendations(List<String> recommendations) { this.recommendations = recommendations; }
}
}

5. Sample k6 Test Scripts

Basic Load Test Script

// src/test/k6/basic-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend, Rate, Counter } from 'k6/metrics';
// Custom metrics
const responseTimeTrend = new Trend('response_time');
const successRate = new Rate('success_rate');
const errorCounter = new Counter('errors');
// Test configuration
export const options = {
stages: [
{ duration: '2m', target: 100 },  // Ramp up to 100 users
{ duration: '5m', target: 100 },  // Stay at 100 users
{ duration: '2m', target: 200 },  // Ramp up to 200 users
{ duration: '5m', target: 200 },  // Stay at 200 users
{ duration: '2m', target: 0 },    // Ramp down to 0 users
],
thresholds: {
'http_req_duration': ['p(95)<500', 'p(99)<1000'],
'http_req_failed': ['rate<0.01'],
'success_rate': ['rate>0.95'],
'response_time': ['p(95)<500']
}
};
// Setup function
export function setup() {
const loginResponse = http.post('http://localhost:8080/api/auth/login', JSON.stringify({
username: 'testuser',
password: 'testpass'
}), {
headers: { 'Content-Type': 'application/json' }
});
const authToken = loginResponse.json('token');
return { authToken };
}
// Main test function
export default function(data) {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${data.authToken}`
};
// Test user profile endpoint
const profileResponse = http.get('http://localhost:8080/api/users/profile', { headers });
check(profileResponse, {
'profile status is 200': (r) => r.status === 200,
'profile response has user data': (r) => r.json('id') !== undefined,
});
responseTimeTrend.add(profileResponse.timings.duration);
successRate.add(profileResponse.status === 200);
if (profileResponse.status !== 200) {
errorCounter.add(1);
}
// Test products endpoint
const productsResponse = http.get('http://localhost:8080/api/products', { headers });
check(productsResponse, {
'products status is 200': (r) => r.status === 200,
'products response has data': (r) => r.json().length > 0,
});
responseTimeTrend.add(productsResponse.timings.duration);
successRate.add(productsResponse.status === 200);
if (productsResponse.status !== 200) {
errorCounter.add(1);
}
sleep(1);
}

API Endpoint Test Script

// src/test/k6/api-endpoints-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend, Counter, Rate } from 'k6/metrics';
// Custom metrics
const authResponseTime = new Trend('auth_response_time');
const userResponseTime = new Trend('user_response_time');
const productResponseTime = new Trend('product_response_time');
const orderResponseTime = new Trend('order_response_time');
const errorRate = new Rate('error_rate');
export const options = {
scenarios: {
auth_flow: {
executor: 'constant-vus',
exec: 'authScenario',
vus: 10,
duration: '5m',
},
user_flow: {
executor: 'ramping-vus',
exec: 'userScenario',
stages: [
{ duration: '2m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '2m', target: 0 },
],
},
browsing_flow: {
executor: 'constant-arrival-rate',
exec: 'browsingScenario',
rate: 10,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 10,
},
},
thresholds: {
'auth_response_time': ['p(95)<300'],
'user_response_time': ['p(95)<400'],
'product_response_time': ['p(95)<500'],
'order_response_time': ['p(95)<800'],
'error_rate': ['rate<0.02']
}
};
export function authScenario() {
const loginData = JSON.stringify({
username: `user_${__VU}@test.com`,
password: 'password123'
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
const loginResponse = http.post('http://localhost:8080/api/auth/login', loginData, params);
check(loginResponse, {
'auth login status is 200': (r) => r.status === 200,
'auth login has token': (r) => r.json('token') !== undefined,
});
authResponseTime.add(loginResponse.timings.duration);
errorRate.add(loginResponse.status !== 200);
sleep(1);
}
export function userScenario() {
// First login
const loginResponse = http.post('http://localhost:8080/api/auth/login', JSON.stringify({
username: `user_${__VU}@test.com`,
password: 'password123'
}), {
headers: { 'Content-Type': 'application/json' }
});
const token = loginResponse.json('token');
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
// Get user profile
const profileResponse = http.get('http://localhost:8080/api/users/profile', { headers });
check(profileResponse, {
'user profile status is 200': (r) => r.status === 200,
});
userResponseTime.add(profileResponse.timings.duration);
// Update user profile
const updateData = JSON.stringify({
name: `Updated User ${__VU}`,
email: `updated_${__VU}@test.com`
});
const updateResponse = http.put('http://localhost:8080/api/users/profile', updateData, { headers });
check(updateResponse, {
'user update status is 200': (r) => r.status === 200,
});
userResponseTime.add(updateResponse.timings.duration);
errorRate.add(profileResponse.status !== 200 || updateResponse.status !== 200);
sleep(2);
}
export function browsingScenario() {
const headers = { 'Content-Type': 'application/json' };
// Browse products
const productsResponse = http.get('http://localhost:8080/api/products', { headers });
check(productsResponse, {
'products list status is 200': (r) => r.status === 200,
'products list has items': (r) => r.json().length > 0,
});
productResponseTime.add(productsResponse.timings.duration);
// Get product details
const productId = Math.floor(Math.random() * 100) + 1;
const productResponse = http.get(`http://localhost:8080/api/products/${productId}`, { headers });
check(productResponse, {
'product details status is 200': (r) => r.status === 200,
});
productResponseTime.add(productResponse.timings.duration);
errorRate.add(productsResponse.status !== 200 || productResponse.status !== 200);
sleep(1);
}

6. JUnit 5 Test Integration

// K6PerformanceTest.java
package com.example.k6.test;
import com.example.k6.generator.K6TestGenerator;
import com.example.k6.runner.K6TestRunner;
import com.example.k6.analysis.K6ResultsAnalyzer;
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 static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class K6PerformanceTest {
@Autowired
private K6TestGenerator testGenerator;
@Autowired
private K6TestRunner testRunner;
@Autowired
private K6ResultsAnalyzer resultsAnalyzer;
@BeforeAll
void setup() {
// Generate test scripts before all tests
generateTestScripts();
}
@Test
@DisplayName("Basic Load Test - 100 Users")
void testBasicLoad() {
K6TestRunner.TestOptions options = new K6TestRunner.TestOptions();
options.setVus(100);
options.setDuration("5m");
K6TestRunner.TestExecutionResult result = testRunner.runTest("basic-load-test.js", options);
assertTrue(result.isSuccess(), "k6 test should complete successfully");
// Analyze results
K6ResultsAnalyzer.TestAnalysis analysis = resultsAnalyzer.analyzeResults(result.getOutputFile());
K6ResultsAnalyzer.PerformanceReport report = resultsAnalyzer.generateReport(analysis);
assertEquals(K6ResultsAnalyzer.PerformanceReport.ReportStatus.PASSED, report.getStatus(),
"Performance test should pass all thresholds");
assertTrue(report.getPerformanceScore() >= 80,
"Performance score should be at least 80");
}
@Test
@DisplayName("API Endpoints Performance Test")
void testApiEndpoints() {
K6TestRunner.TestExecutionResult result = testRunner.runTest("api-endpoints-test.js", 
new K6TestRunner.TestOptions());
assertTrue(result.isSuccess(), "API endpoints test should complete successfully");
K6ResultsAnalyzer.TestAnalysis analysis = resultsAnalyzer.analyzeResults(result.getOutputFile());
K6ResultsAnalyzer.PerformanceReport report = resultsAnalyzer.generateReport(analysis);
System.out.println("Performance Score: " + report.getPerformanceScore());
System.out.println("Recommendations: " + report.getRecommendations());
assertTrue(report.getPerformanceScore() >= 70,
"API endpoints should meet minimum performance standards");
}
@Test
@DisplayName("Stress Test - 500 Concurrent Users")
void testStressScenario() {
K6TestGenerator.LoadTestConfig config = new K6TestGenerator.LoadTestConfig();
// Define stress test stages
config.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("2m", 100));
config.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("3m", 300));
config.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("2m", 500));
config.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("1m", 500));
config.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("2m", 0));
// Generate stress test script
testGenerator.generateAndSaveTestScript(config, "stress-test.js");
// Run the test
K6TestRunner.TestExecutionResult result = testRunner.runTest("stress-test.js", 
new K6TestRunner.TestOptions());
assertTrue(result.isSuccess(), "Stress test should complete successfully");
}
private void generateTestScripts() {
// Generate basic load test
K6TestGenerator.LoadTestConfig basicConfig = new K6TestGenerator.LoadTestConfig();
basicConfig.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("1m", 50));
basicConfig.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("3m", 50));
basicConfig.getStages().add(new K6TestGenerator.LoadTestConfig.Stage("1m", 0));
K6TestGenerator.LoadTestConfig.Scenario basicScenario = 
new K6TestGenerator.LoadTestConfig.Scenario("Basic API Calls");
K6TestGenerator.LoadTestConfig.Step getUsers = new K6TestGenerator.LoadTestConfig.Step();
getUsers.setType("http");
getUsers.setMethod("GET");
getUsers.setUrl("http://localhost:8080/api/users");
getUsers.setExpectedStatus(200);
basicScenario.getSteps().add(getUsers);
basicConfig.getScenarios().add(basicScenario);
testGenerator.generateAndSaveTestScript(basicConfig, "generated-basic-test.js");
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("k6.scripts.directory", () -> "src/test/k6");
registry.add("k6.results.directory", () -> "target/k6-results");
}
}

This comprehensive k6 integration provides:

  • Test script generation from Java configuration
  • Programmatic test execution with various options
  • Results analysis and performance reporting
  • Custom metrics collection
  • Threshold validation and performance scoring
  • JUnit 5 integration for CI/CD pipelines
  • Docker support for k6 execution
  • Multiple test scenarios (load, stress, spike)

The setup enables comprehensive performance testing of Java applications with detailed analysis and reporting capabilities.

Leave a Reply

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


Macro Nepal Helper