// k6 Load Test Configuration
// Run: k6 run load-test.js --env BASE_URL=https://api.example.com
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const apiDuration = new Trend('api_duration', true);
const successfulLogins = new Counter('successful_logins');
// Test configuration with multiple scenarios
export const options = {
scenarios: {
// Scenario 1: Smoke test (sanity check)
smoke: {
executor: 'constant-vus',
vus: 1,
duration: '30s',
startTime: '0s',
tags: { test_type: 'smoke' },
},
// Scenario 2: Load test (normal traffic)
load: {
executor: 'ramping-vus',
startTime: '30s',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Stay at 50 for 5 min
{ duration: '2m', target: 100 }, // Ramp up to 100
{ duration: '5m', target: 100 }, // Stay at 100 for 5 min
{ duration: '2m', target: 0 }, // Ramp down
],
tags: { test_type: 'load' },
},
// Scenario 3: Spike test
spike: {
executor: 'ramping-vus',
startTime: '17m',
startVUs: 0,
stages: [
{ duration: '10s', target: 500 }, // Sudden spike
{ duration: '1m', target: 500 }, // Hold spike
{ duration: '10s', target: 0 }, // Quick ramp down
],
tags: { test_type: 'spike' },
},
},
// Performance thresholds (fail test if not met)
thresholds: {
http_req_duration: [
'p(95)<500', // 95% of requests under 500ms
'p(99)<1500', // 99% of requests under 1.5s
],
http_req_failed: ['rate<0.01'], // Error rate under 1%
errors: ['rate<0.05'], // Custom error rate under 5%
api_duration: ['avg<300'], // Average API call under 300ms
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
// Setup: runs once before the test
export function setup() {
const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: 'loadtest@example.com',
password: 'test-password',
}), { headers: { 'Content-Type': 'application/json' } });
const token = loginRes.json('token');
return { token };
}
// Main test function: runs for each VU iteration
export default function (data) {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${data.token}`,
};
group('API Health', () => {
const res = http.get(`${BASE_URL}/api/health`);
check(res, {
'health check status is 200': (r) => r.status === 200,
'health check response time < 100ms': (r) => r.timings.duration < 100,
});
errorRate.add(res.status !== 200);
});
group('List Resources', () => {
const res = http.get(`${BASE_URL}/api/snips?page=1&per_page=20`, { headers });
const success = check(res, {
'list status is 200': (r) => r.status === 200,
'list returns array': (r) => Array.isArray(r.json('data')),
'list response time < 500ms': (r) => r.timings.duration < 500,
});
apiDuration.add(res.timings.duration);
errorRate.add(!success);
});
group('Get Single Resource', () => {
const res = http.get(`${BASE_URL}/api/snips/1`, { headers });
check(res, {
'get status is 200': (r) => r.status === 200,
'response has title': (r) => r.json('title') !== undefined,
});
apiDuration.add(res.timings.duration);
errorRate.add(res.status !== 200);
});
group('Search', () => {
const queries = ['docker', 'kubernetes', 'terraform', 'nginx'];
const query = queries[Math.floor(Math.random() * queries.length)];
const res = http.get(`${BASE_URL}/api/search?q=${query}`, { headers });
check(res, {
'search status is 200': (r) => r.status === 200,
'search returns results': (r) => r.json('data').length >= 0,
'search response time < 800ms': (r) => r.timings.duration < 800,
});
apiDuration.add(res.timings.duration);
});
sleep(Math.random() * 2 + 1); // Think time: 1-3 seconds
}
// Teardown: runs once after the test
export function teardown(data) {
console.log('Load test complete. Review results in Grafana or CI output.');
}