Introduce AuditLog model to track system-wide user actions including authentication, course management, file operations, and user account changes. Add AuditAction enum with 17 action types (CREATE, UPDATE, DELETE, LOGIN, LOGOUT, ENROLL, UNENROLL, SUBMIT_QUIZ, APPROVE_COURSE, REJECT_COURSE, UPLOAD_FILE, DELETE_FILE, CHANGE_PASSWORD, RESET_PASSWORD, VERIFY_EMAIL, DEACTIVATE_USER, ACTIVATE_USER). Include fields
11 KiB
11 KiB
| description |
|---|
| How to create k6 load tests for API endpoints |
K6 Load Test Workflow
วิธีสร้าง k6 load test สำหรับ API endpoints ใน E-Learning Backend
Prerequisites
- ติดตั้ง k6:
# macOS
brew install k6
# หรือ download จาก https://k6.io/docs/getting-started/installation/
- สร้าง folder สำหรับ tests:
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
// Backend/tests/k6/<test-name>.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
// 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)
// 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
// 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)
// 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
# รันจาก Backend directory
k6 run tests/k6/<test-name>.js
With Environment Variables
# ระบุ BASE_URL
k6 run -e BASE_URL=http://localhost:3000 tests/k6/<test-name>.js
# ระบุ VUs และ Duration แบบ simple
k6 run --vus 10 --duration 30s tests/k6/<test-name>.js
Output to JSON
k6 run --out json=results.json tests/k6/<test-name>.js
Output to InfluxDB (for Grafana)
k6 run --out influxdb=http://localhost:8086/k6 tests/k6/<test-name>.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 ที่แนะนำ:
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
- Test Data: สร้าง test users ก่อนรัน load test
- Warm-up: ใช้ ramp-up stages เพื่อไม่ให้ server shock
- Think Time: ใส่
sleep()เพื่อจำลอง user behavior จริง - Isolation: รัน test บน environment แยก ไม่ใช่ production
- Baseline: รัน test หลายรอบเพื่อหา baseline performance
- Monitor: ดู server metrics (CPU, Memory, DB connections) ขณะรัน test