--- description: How to create k6 load tests for API endpoints --- # K6 Load Test Workflow วิธีสร้าง k6 load test สำหรับ API endpoints ใน E-Learning Backend ## Prerequisites 1. ติดตั้ง k6: ```bash # macOS brew install k6 # หรือ download จาก https://k6.io/docs/getting-started/installation/ ``` 2. สร้าง folder สำหรับ tests: ```bash mkdir -p Backend/tests/k6 ``` ## Step 1: ระบุ API Endpoints ที่ต้องการ Test ดู API endpoints จาก: - `Backend/src/controllers/` - ดู Route decorators (@Route, @Get, @Post, etc.) - `Backend/public/swagger.json` - ดู OpenAPI spec (ถ้ามี) ### Available Controllers & Endpoints: | Controller | Base Route | Key Endpoints | |------------|------------|---------------| | AuthController | `/api/auth` | `POST /login`, `POST /register-learner`, `POST /register-instructor`, `POST /refresh` | | CoursesStudentController | `/api/students` | `GET /courses`, `POST /courses/{id}/enroll`, `GET /courses/{id}/learn` | | CoursesInstructorController | `/api/instructors/courses` | `GET /`, `GET /{id}`, `POST /`, `PUT /{id}` | | CategoriesController | `/api/categories` | `GET /`, `GET /{id}` | | UserController | `/api/users` | `GET /me`, `PUT /me` | | CertificateController | `/api/certificates` | `GET /{id}` | ## Step 2: สร้าง K6 Test Script สร้างไฟล์ใน `Backend/tests/k6/` ตาม template นี้: ### Template: Basic Load Test ```javascript // Backend/tests/k6/.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 responseTime = new Trend('response_time'); // Test configuration export const options = { // Ramp-up pattern stages: [ { duration: '30s', target: 10 }, // Ramp up to 10 users { duration: '1m', target: 10 }, // Stay at 10 users { duration: '30s', target: 50 }, // Ramp up to 50 users { duration: '1m', target: 50 }, // Stay at 50 users { duration: '30s', target: 0 }, // Ramp down ], thresholds: { http_req_duration: ['p(95)<500'], // 95% of requests < 500ms errors: ['rate<0.1'], // Error rate < 10% }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; // Setup: Get auth token (runs once per VU) export function setup() { const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: 'test@example.com', password: 'password123', }), { headers: { 'Content-Type': 'application/json' }, }); const token = loginRes.json('data.token'); return { token }; } // Main test function export default function (data) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}`, }; // Test endpoint const res = http.get(`${BASE_URL}/api/endpoint`, { headers }); // Record metrics responseTime.add(res.timings.duration); errorRate.add(res.status !== 200); // Assertions check(res, { 'status is 200': (r) => r.status === 200, 'response time < 500ms': (r) => r.timings.duration < 500, }); sleep(1); // Think time between requests } ``` ## Step 3: Test Scenarios ตามประเภท ### 3.1 Authentication Load Test ```javascript // Backend/tests/k6/auth-load-test.js import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate } from 'k6/metrics'; const errorRate = new Rate('errors'); export const options = { stages: [ { duration: '30s', target: 20 }, { duration: '1m', target: 20 }, { duration: '30s', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<1000'], errors: ['rate<0.05'], }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; export default function () { // Test Login const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: `user${__VU}@test.com`, password: 'password123', }), { headers: { 'Content-Type': 'application/json' }, }); errorRate.add(loginRes.status !== 200); check(loginRes, { 'login successful': (r) => r.status === 200, 'has token': (r) => r.json('data.token') !== undefined, }); sleep(1); } ``` ### 3.2 Course Browsing Load Test (Student) ```javascript // Backend/tests/k6/student-courses-load-test.js import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate, Trend } from 'k6/metrics'; const errorRate = new Rate('errors'); const courseListTime = new Trend('course_list_time'); const courseLearningTime = new Trend('course_learning_time'); export const options = { stages: [ { duration: '30s', target: 30 }, { duration: '2m', target: 30 }, { duration: '30s', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<800'], errors: ['rate<0.1'], }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; export function setup() { const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: 'student@test.com', password: 'password123', }), { headers: { 'Content-Type': 'application/json' }, }); return { token: loginRes.json('data.token') }; } export default function (data) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}`, }; group('List Enrolled Courses', () => { const res = http.get(`${BASE_URL}/api/students/courses`, { headers }); courseListTime.add(res.timings.duration); errorRate.add(res.status !== 200); check(res, { 'courses listed': (r) => r.status === 200 }); }); sleep(2); group('Get Course Learning Page', () => { const courseId = 1; // Use a valid course ID const res = http.get(`${BASE_URL}/api/students/courses/${courseId}/learn`, { headers }); courseLearningTime.add(res.timings.duration); errorRate.add(res.status !== 200 && res.status !== 403); check(res, { 'learning page loaded': (r) => r.status === 200 || r.status === 403 }); }); sleep(1); } ``` ### 3.3 Instructor Course Management Load Test ```javascript // Backend/tests/k6/instructor-courses-load-test.js import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate } from 'k6/metrics'; const errorRate = new Rate('errors'); export const options = { stages: [ { duration: '30s', target: 10 }, { duration: '1m', target: 10 }, { duration: '30s', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<1000'], errors: ['rate<0.1'], }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; export function setup() { const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: 'instructor@test.com', password: 'password123', }), { headers: { 'Content-Type': 'application/json' }, }); return { token: loginRes.json('data.token') }; } export default function (data) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}`, }; group('List My Courses', () => { const res = http.get(`${BASE_URL}/api/instructors/courses`, { headers }); errorRate.add(res.status !== 200); check(res, { 'courses listed': (r) => r.status === 200 }); }); sleep(2); group('Get Course Detail', () => { const courseId = 1; const res = http.get(`${BASE_URL}/api/instructors/courses/${courseId}`, { headers }); errorRate.add(res.status !== 200 && res.status !== 404); check(res, { 'course detail loaded': (r) => r.status === 200 || r.status === 404 }); }); sleep(1); } ``` ### 3.4 Mixed Scenario (Realistic Load) ```javascript // Backend/tests/k6/mixed-load-test.js import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate } from 'k6/metrics'; import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; const errorRate = new Rate('errors'); export const options = { scenarios: { students: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '30s', target: 50 }, { duration: '2m', target: 50 }, { duration: '30s', target: 0 }, ], exec: 'studentScenario', }, instructors: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '30s', target: 10 }, { duration: '2m', target: 10 }, { duration: '30s', target: 0 }, ], exec: 'instructorScenario', }, }, thresholds: { http_req_duration: ['p(95)<1000'], errors: ['rate<0.1'], }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; function getToken(email, password) { const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email, password }), { headers: { 'Content-Type': 'application/json' }, }); return res.json('data.token'); } export function studentScenario() { const token = getToken('student@test.com', 'password123'); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }; // Browse courses const coursesRes = http.get(`${BASE_URL}/api/students/courses`, { headers }); check(coursesRes, { 'student courses ok': (r) => r.status === 200 }); sleep(randomIntBetween(1, 3)); } export function instructorScenario() { const token = getToken('instructor@test.com', 'password123'); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }; // List courses const coursesRes = http.get(`${BASE_URL}/api/instructors/courses`, { headers }); check(coursesRes, { 'instructor courses ok': (r) => r.status === 200 }); sleep(randomIntBetween(2, 5)); } ``` ## Step 4: รัน Load Test ### Basic Run ```bash # รันจาก Backend directory k6 run tests/k6/.js ``` ### With Environment Variables ```bash # ระบุ BASE_URL k6 run -e BASE_URL=http://localhost:3000 tests/k6/.js # ระบุ VUs และ Duration แบบ simple k6 run --vus 10 --duration 30s tests/k6/.js ``` ### Output to JSON ```bash k6 run --out json=results.json tests/k6/.js ``` ### Output to InfluxDB (for Grafana) ```bash k6 run --out influxdb=http://localhost:8086/k6 tests/k6/.js ``` ## Step 5: วิเคราะห์ผลลัพธ์ ### Key Metrics to Watch: - **http_req_duration**: Response time (p50, p90, p95, p99) - **http_req_failed**: Failed request rate - **http_reqs**: Requests per second (throughput) - **vus**: Virtual users at any point - **iterations**: Total completed iterations ### Thresholds ที่แนะนำ: ```javascript thresholds: { http_req_duration: ['p(95)<500'], // 95% < 500ms http_req_duration: ['p(99)<1000'], // 99% < 1s http_req_failed: ['rate<0.01'], // < 1% errors http_reqs: ['rate>100'], // > 100 req/s } ``` ## Tips & Best Practices 1. **Test Data**: สร้าง test users ก่อนรัน load test 2. **Warm-up**: ใช้ ramp-up stages เพื่อไม่ให้ server shock 3. **Think Time**: ใส่ `sleep()` เพื่อจำลอง user behavior จริง 4. **Isolation**: รัน test บน environment แยก ไม่ใช่ production 5. **Baseline**: รัน test หลายรอบเพื่อหา baseline performance 6. **Monitor**: ดู server metrics (CPU, Memory, DB connections) ขณะรัน test