Comprehensive Load Testing: Using Artillery for Java Applications

Article

Load testing is crucial for ensuring Java applications can handle production traffic while maintaining performance and reliability. Artillery is a modern, powerful load testing tool that enables developers to create realistic load tests for HTTP, WebSocket, and other protocols. When combined with Java applications, it provides a complete solution for performance validation.


What is Artillery?

Artillery is an open-source load testing toolkit designed for distributed load testing and smoke testing of applications. It uses YAML/JSON for test configuration and JavaScript for complex test logic, making it highly flexible and developer-friendly.

Key Advantages for Java Teams:

  • Simple YAML Configuration: Easy to write and maintain test scenarios
  • JavaScript Extensibility: Custom logic for complex test flows
  • Multiple Protocols: HTTP, WebSocket, Socket.IO, and more
  • Rich Metrics: Detailed performance reports and metrics
  • CI/CD Integration: Easy integration with testing pipelines
  • Distributed Testing: Scale tests across multiple machines

Installation and Setup

1. Install Artillery

# Install via npm (requires Node.js)
npm install -g artillery
# Or as development dependency
npm install --save-dev artillery
# Verify installation
artillery --version
# Install plugins
npm install -g artillery-plugin-metrics-by-endpoint
npm install -g artillery-plugin-ensure

2. Docker Installation

# Run Artillery via Docker
docker run --rm -it -v $(pwd):/tests artilleryio/artillery:latest run /tests/load-test.yml

3. Java Application Setup

Ensure your Java application is running and accessible:

# Spring Boot example
./mvnw spring-boot:run
# Or run built JAR
java -jar target/my-java-app-1.0.0.jar

Basic Load Test Configuration

1. Simple HTTP Load Test

basic-load-test.yml:

config:
target: "http://localhost:8080"
phases:
- duration: 60
arrivalRate: 10
name: "Warm up phase"
- duration: 120
arrivalRate: 50
name: "Load phase"
- duration: 60
arrivalRate: 10
name: "Cool down phase"
plugins:
metrics-by-endpoint: {}
ensure: {}
ensure:
thresholds:
- http.response_time.p99: 500
- http.response_time.median: 200
- http.requests: 5000
scenarios:
- name: "Basic API Test"
flow:
- get:
url: "/api/health"
capture:
json: "$.status"
as: "health_status"
expect:
- statusCode: 200
- contentType: "application/json"
- think: 2  # Wait 2 seconds
- get:
url: "/api/users"
expect:
- statusCode: 200
- post:
url: "/api/users"
json:
name: "Test User"
email: "[email protected]"
expect:
- statusCode: 201

2. Run the Test

# Run basic test
artillery run basic-load-test.yml
# Run with JSON output
artillery run basic-load-test.yml --output report.json
# Generate HTML report
artillery run basic-load-test.yml --output report.json
artillery report report.json

Advanced Java Application Testing

1. Spring Boot REST API Load Test

spring-boot-load-test.yml:

config:
target: "http://localhost:8080"
phases:
- duration: 300
arrivalRate: 20
rampTo: 100
name: "Ramp up to peak load"
plugins:
metrics-by-endpoint: {}
ensure: {}
publish-metrics:
- type: "datadog"
apiKey: "${DATADOG_API_KEY}"
appKey: "${DATADOG_APP_KEY}"
ensure:
thresholds:
- http.response_time.p99: 1000
- http.response_time.median: 300
- http.downloaded_bytes: 500000
payload:
path: "test-data/users.csv"
fields:
- "username"
- "email"
- "userId"
order: "random"
scenarios:
- name: "Spring Boot REST API Test"
flow:
# Health check
- get:
url: "/actuator/health"
name: "Health Check"
expect:
- statusCode: 200
- hasProperty: "status", equals: "UP"
# Get all users
- get:
url: "/api/v1/users"
name: "Get Users"
query:
page: 0
size: 10
expect:
- statusCode: 200
- contentType: "application/json"
# Create user with dynamic data
- post:
url: "/api/v1/users"
name: "Create User"
json:
username: "{{ username }}"
email: "{{ email }}"
role: "USER"
capture:
json: "$.id"
as: "userId"
expect:
- statusCode: 201
# Get specific user
- get:
url: "/api/v1/users/{{ userId }}"
name: "Get User by ID"
expect:
- statusCode: 200
# Update user
- put:
url: "/api/v1/users/{{ userId }}"
name: "Update User"
json:
username: "{{ username }}_updated"
email: "updated_{{ email }}"
expect:
- statusCode: 200
# Delete user
- delete:
url: "/api/v1/users/{{ userId }}"
name: "Delete User"
expect:
- statusCode: 204
# Search users
- get:
url: "/api/v1/users/search"
name: "Search Users"
query:
query: "test"
expect:
- statusCode: 200

2. Test Data File (users.csv)

username,email,userId
john_doe,[email protected],1001
jane_smith,[email protected],1002
bob_wilson,[email protected],1003
alice_brown,[email protected],1004
charlie_davis,[email protected],1005

Java-Specific Test Scenarios

1. JWT Authentication Flow

jwt-auth-test.yml:

config:
target: "https://api.myjavaapp.com"
phases:
- duration: 180
arrivalRate: 30
name: "Authentication Load Test"
processor: "./auth-hooks.js"
scenarios:
- name: "JWT Authentication Flow"
flow:
# Login and get token
- post:
url: "/api/auth/login"
json:
username: "{{ username }}"
password: "{{ password }}"
capture:
json: "$.accessToken"
as: "authToken"
expect:
- statusCode: 200
- function: "setAuthHeader"
# Access protected endpoints
- get:
url: "/api/protected/user-profile"
expect:
- statusCode: 200
- get:
url: "/api/protected/orders"
expect:
- statusCode: 200
# Refresh token
- post:
url: "/api/auth/refresh"
json:
refreshToken: "{{ refreshToken }}"
capture:
json: "$.accessToken"
as: "authToken"
expect:
- statusCode: 200
- function: "setAuthHeader"

auth-hooks.js:

module.exports = { 
setAuthHeader,
generateTestData 
};
function setAuthHeader(requestParams, context, ee, next) {
// Add Authorization header with JWT token
requestParams.headers = requestParams.headers || {};
requestParams.headers['Authorization'] = `Bearer ${context.vars.authToken}`;
return next();
}
function generateTestData(requestParams, context, ee, next) {
// Generate dynamic test data
context.vars.username = `user_${Math.floor(Math.random() * 10000)}`;
context.vars.password = `pass_${Math.floor(Math.random() * 100000)}`;
context.vars.refreshToken = `refresh_${Math.floor(Math.random() * 1000000)}`;
return next();
}

2. Database-Backed API Test

database-api-test.yml:

config:
target: "http://localhost:8080"
phases:
- duration: 300
arrivalRate: 50
name: "Database API Load"
payload:
path: "test-data/products.csv"
fields:
- "productId"
- "name"
- "price"
order: "random"
scenarios:
- name: "Product API Load Test"
flow:
# Get all products
- get:
url: "/api/products"
name: "Get Products"
expect:
- statusCode: 200
# Get specific product
- get:
url: "/api/products/{{ productId }}"
name: "Get Product by ID"
expect:
- statusCode: 200
# Create order
- post:
url: "/api/orders"
name: "Create Order"
json:
productId: "{{ productId }}"
quantity: 1
customerEmail: "[email protected]"
capture:
json: "$.orderId"
as: "orderId"
expect:
- statusCode: 201
# Get order status
- get:
url: "/api/orders/{{ orderId }}"
name: "Get Order Status"
expect:
- statusCode: 200

WebSocket Testing for Java Applications

1. Spring WebSocket Load Test

websocket-test.yml:

config:
target: "ws://localhost:8080"
phases:
- duration: 120
arrivalRate: 10
name: "WebSocket Connection Test"
engine: "ws"
scenarios:
- name: "Real-time Chat Load Test"
engine: "ws"
flow:
# Connect to WebSocket endpoint
- connect: "/ws/chat"
# Send authentication message
- send:
data: |
{
"type": "AUTH",
"username": "user_{{ $randomNumber(1000, 9999) }}",
"token": "test_token"
}
# Wait for authentication response
- think: 1
# Join chat room
- send:
data: |
{
"type": "JOIN",
"room": "general"
}
# Send multiple chat messages
- loop:
- send:
data: |
{
"type": "MESSAGE",
"content": "Test message {{ $loopCount }}",
"room": "general"
}
- think: 2
count: 5
# Listen for incoming messages
- think: 10
# Disconnect
- close: true

2. Socket.IO Testing

socketio-test.yml:

config:
target: "http://localhost:3000"
phases:
- duration: 180
arrivalRate: 20
name: "Socket.IO Load Test"
engine: "socketio"
scenarios:
- name: "Real-time Notifications"
engine: "socketio"
flow:
- emit:
channel: "authenticate"
data: 
token: "user_token_{{ $randomNumber(1000, 9999) }}"
- emit:
channel: "join_room"
data:
room: "notifications"
- think: 2
- emit:
channel: "send_notification"
data:
message: "Test notification {{ $timestamp }}"
type: "info"
- on:
channel: "new_notification"
capture:
json: "$.message"
as: "notification_message"

Custom Metrics and Hooks for Java Apps

1. Custom Response Validation

custom-validation.js:

module.exports = {
validateSpringResponse,
captureBusinessMetrics,
logSlowRequests
};
function validateSpringResponse(requestParams, response, context, ee, next) {
// Validate Spring Boot specific responses
if (response.body) {
try {
const body = JSON.parse(response.body);
// Check for Spring Data REST specific structures
if (body._embedded) {
context.vars.hasEmbedded = true;
}
// Check for Spring HATEOAS links
if (body._links) {
context.vars.hasLinks = true;
}
// Validate pagination structure
if (body.page) {
const { size, totalElements, totalPages } = body.page;
if (totalElements > 10000) {
console.warn(`Large dataset detected: ${totalElements} elements`);
}
}
} catch (e) {
// Ignore JSON parse errors
}
}
return next();
}
function captureBusinessMetrics(requestParams, response, context, ee, next) {
// Capture business-specific metrics
if (response.timings) {
const responseTime = response.timings.phases.firstByte;
// Categorize response times
if (responseTime > 1000) {
ee.emit('counter', 'slow_responses', 1);
} else if (responseTime < 100) {
ee.emit('counter', 'fast_responses', 1);
}
// Track endpoint-specific metrics
const endpoint = requestParams.url || 'unknown';
ee.emit('histogram', `response_time.${endpoint}`, responseTime);
}
return next();
}
function logSlowRequests(requestParams, response, context, ee, next) {
const slowThreshold = 2000; // 2 seconds
if (response.timings && response.timings.phases.firstByte > slowThreshold) {
console.log(`SLOW REQUEST: ${requestParams.method} ${requestParams.url} - ${response.timings.phases.firstByte}ms`);
}
return next();
}

2. Enhanced Load Test Configuration

advanced-java-test.yml:

config:
target: "http://localhost:8080"
phases:
- duration: 60
arrivalRate: 5
name: "Smoke test"
- duration: 300
arrivalRate: 10
rampTo: 100
name: "Load test"
- duration: 60
arrivalRate: 200
name: "Spike test"
processor: "./custom-validation.js"
plugins:
metrics-by-endpoint: {}
ensure: {}
apdex: {}
apdex:
threshold: 500
ensure:
thresholds:
- http.response_time.p95: 800
- http.response_time.median: 300
- http.request_rate: 50
- apdex: 0.9
scenarios:
- name: "Comprehensive API Test"
beforeRequest: "captureBusinessMetrics"
afterResponse: "validateSpringResponse"
flow:
- get:
url: "/api/health"
afterResponse: "logSlowRequests"
- post:
url: "/api/auth/login"
json:
username: "load_test_user"
password: "test_password"
capture:
json: "$.token"
as: "authToken"

Distributed Load Testing

1. Multi-Node Testing Setup

docker-compose-distributed.yml:

version: '3.8'
services:
artillery-primary:
image: artilleryio/artillery:latest
volumes:
- ./tests:/tests
environment:
- ARTILLERY_REDIS_HOST=redis
command: run /tests/distributed-test.yml --config /tests/artillery-config.yml
depends_on:
- redis
networks:
- load-test
artillery-worker-1:
image: artilleryio/artillery:latest
volumes:
- ./tests:/tests
environment:
- ARTILLERY_REDIS_HOST=redis
command: run-worker --config /tests/artillery-config.yml
depends_on:
- redis
networks:
- load-test
artillery-worker-2:
image: artilleryio/artillery:latest
volumes:
- ./tests:/tests
environment:
- ARTILLERY_REDIS_HOST=redis
command: run-worker --config /tests/artillery-config.yml
depends_on:
- redis
networks:
- load-test
redis:
image: redis:7-alpine
networks:
- load-test
networks:
load-test:
driver: bridge

2. Distributed Test Configuration

artillery-config.yml:

config:
environments:
primary:
target: "http://java-app:8080"
phases:
- duration: 600
arrivalRate: 100
processor: "./distributed-hooks.js"
worker:
target: "http://java-app:8080"
redis:
host: "redis"
port: 6379
plugins:
publish-metrics:
- type: "datadog"
apiKey: "${DATADOG_API_KEY}"
- type: "prometheus"
pushgateway: "http://prometheus:9091"

CI/CD Integration

1. GitHub Actions Workflow

name: Load Test Java Application
on:
push:
branches: [ main ]
schedule:
- cron: '0 2 * * *'  # Daily at 2 AM
jobs:
load-test:
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Artillery
run: npm install -g artillery artillery-plugin-metrics-by-endpoint
- name: Start Java Application
run: |
docker run -d -p 8080:8080 --name java-app my-company/my-java-app:latest
sleep 30  # Wait for app to start
- name: Run Smoke Test
run: artillery run tests/smoke-test.yml
- name: Run Load Test
run: artillery run tests/load-test.yml --output results.json
- name: Generate Report
run: artillery report results.json --output report.html
- name: Upload Report
uses: actions/upload-artifact@v3
with:
name: load-test-report
path: report.html
- name: Check Performance Thresholds
run: |
artillery run tests/load-test.yml --tags "thresholds"

2. Jenkins Pipeline

pipeline {
agent any
environment {
ARTILLERY_CONFIG = 'tests/ci-config.yml'
}
stages {
stage('Build Java App') {
steps {
sh 'mvn clean package'
}
}
stage('Deploy to Test') {
steps {
sh 'docker-compose -f docker-compose.test.yml up -d'
sh 'sleep 60' // Wait for app to start
}
}
stage('Run Load Tests') {
steps {
sh '''
npm install -g artillery
artillery run ${ARTILLERY_CONFIG} --output test-results.json
artillery report test-results.json
'''
}
}
stage('Performance Gates') {
steps {
sh '''
artillery run ${ARTILLERY_CONFIG} --tags "thresholds"
'''
}
}
}
post {
always {
sh 'docker-compose -f docker-compose.test.yml down'
archiveArtifacts artifacts: 'test-results.json, report.html'
}
}
}

Best Practices for Java Load Testing

1. Test Data Management

config:
payload:
path: "test-data/{{ $environment }}-users.csv"
fields:
- "username"
- "userId"
order: "random"

2. Environment-Specific Configuration

config:
environments:
dev:
target: "http://dev-api:8080"
phases:
- duration: 60
arrivalRate: 10
staging:
target: "https://staging-api.company.com"
phases:
- duration: 300
arrivalRate: 50
prod:
target: "https://api.company.com"
phases:
- duration: 600
arrivalRate: 100

3. Monitoring Integration

config:
plugins:
publish-metrics:
- type: "datadog"
apiKey: "${DATADOG_API_KEY}"
- type: "prometheus"
pushgateway: "http://prometheus:9091"
- type: "influxdb"
url: "http://influxdb:8086"
database: "load_tests"

Conclusion

Artillery provides Java development teams with a powerful, flexible load testing solution that integrates seamlessly into modern development workflows. Key advantages include:

  • Developer-Friendly: YAML configuration with JavaScript extensibility
  • Java Ecosystem Integration: Perfect for Spring Boot, Micronaut, Quarkus applications
  • Comprehensive Protocol Support: HTTP, WebSocket, Socket.IO for full-stack testing
  • CI/CD Ready: Easy integration with Jenkins, GitHub Actions, and other pipelines
  • Production Insights: Rich metrics and reporting for performance analysis

For Java applications, Artillery enables teams to proactively identify performance bottlenecks, validate scalability requirements, and ensure reliable operation under production loads, making it an essential tool for modern Java development.

Leave a Reply

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


Macro Nepal Helper