/** * K6 Load Test - Rate Search Endpoint * * Target: 100 requests/second * Duration: 5 minutes * * Run: k6 run rate-search.test.js */ import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate, Trend } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('errors'); const searchDuration = new Trend('search_duration'); // Test configuration export const options = { stages: [ { duration: '1m', target: 20 }, // Ramp up to 20 users { duration: '2m', target: 50 }, // Ramp up to 50 users { duration: '1m', target: 100 }, // Ramp up to 100 users { duration: '3m', target: 100 }, // Stay at 100 users { duration: '1m', target: 0 }, // Ramp down ], thresholds: { http_req_duration: ['p(95)<2000'], // 95% of requests must complete below 2s http_req_failed: ['rate<0.01'], // Error rate must be less than 1% errors: ['rate<0.05'], // Business error rate must be less than 5% }, }; // Base URL const BASE_URL = __ENV.API_URL || 'http://localhost:4000/api/v1'; // Auth token (should be set via environment variable) const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''; // Test data - common trade lanes const tradeLanes = [ { origin: 'NLRTM', // Rotterdam destination: 'CNSHA', // Shanghai containerType: '40HC', }, { origin: 'USNYC', // New York destination: 'GBLON', // London containerType: '20ST', }, { origin: 'SGSIN', // Singapore destination: 'USOAK', // Oakland containerType: '40ST', }, { origin: 'DEHAM', // Hamburg destination: 'BRRIO', // Rio de Janeiro containerType: '40HC', }, { origin: 'AEDXB', // Dubai destination: 'INMUN', // Mumbai containerType: '20ST', }, ]; export default function () { // Select random trade lane const tradeLane = tradeLanes[Math.floor(Math.random() * tradeLanes.length)]; // Prepare request payload const payload = JSON.stringify({ origin: tradeLane.origin, destination: tradeLane.destination, departureDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 2 weeks from now containers: [ { type: tradeLane.containerType, quantity: 1, }, ], }); const params = { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${AUTH_TOKEN}`, }, tags: { name: 'RateSearch' }, }; // Make request const startTime = Date.now(); const response = http.post(`${BASE_URL}/rates/search`, payload, params); const duration = Date.now() - startTime; // Record metrics searchDuration.add(duration); // Check response const success = check(response, { 'status is 200': r => r.status === 200, 'response has quotes': r => { try { const body = JSON.parse(r.body); return body.quotes && body.quotes.length > 0; } catch (e) { return false; } }, 'response time < 2s': r => duration < 2000, }); errorRate.add(!success); // Small delay between requests sleep(1); } export function handleSummary(data) { return { stdout: textSummary(data, { indent: ' ', enableColors: true }), 'load-test-results/rate-search-summary.json': JSON.stringify(data), }; } function textSummary(data, options) { const indent = options.indent || ''; const enableColors = options.enableColors || false; return ` ${indent}Test Summary - Rate Search Load Test ${indent}===================================== ${indent} ${indent}Total Requests: ${data.metrics.http_reqs.values.count} ${indent}Failed Requests: ${data.metrics.http_req_failed.values.rate * 100}% ${indent} ${indent}Response Times: ${indent} Average: ${data.metrics.http_req_duration.values.avg.toFixed(2)}ms ${indent} Median: ${data.metrics.http_req_duration.values.med.toFixed(2)}ms ${indent} 95th: ${data.metrics.http_req_duration.values['p(95)'].toFixed(2)}ms ${indent} 99th: ${data.metrics.http_req_duration.values['p(99)'].toFixed(2)}ms ${indent} ${indent}Requests/sec: ${data.metrics.http_reqs.values.rate.toFixed(2)} ${indent} ${indent}Business Metrics: ${indent} Error Rate: ${(data.metrics.errors.values.rate * 100).toFixed(2)}% ${indent} Avg Search Duration: ${data.metrics.search_duration.values.avg.toFixed(2)}ms `; }