elearning/.agent/workflows/k6-load-test.md
JakkrapartXD f7330a7b27 feat: add recommended courses and quiz multiple attempts
- Add is_recommended field to Course model
- Add allow_multiple_attempts field to Quiz model
- Create RecommendedCoursesController for admin management
  - List all approved courses
  - Get course by ID
  - Toggle recommendation status
- Add is_recommended filter to CoursesService.ListCourses
- Add allow_multiple_attempts to quiz update and response types
- Update ChaptersLessonService.updateQuiz to support allow_multiple_attempts
2026-02-11 15:01:58 +07:00

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

  1. ติดตั้ง k6:
# macOS
brew install k6

# หรือ download จาก https://k6.io/docs/getting-started/installation/
  1. สร้าง 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

  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