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.