Skip to content

📊 Lộ trình học k6 chi tiết (4 tuần)

Tuần 1: Kiến thức nền tảng & Script cơ bản

Ngày 1-2: Tổng quan về Load Testing với k6

 1. Understand why load testing is needed
 - Measure system performance
 - Identify bottlenecks
 - Test scalability
 - Ensure SLA compliance (Service Level Agreement)

 2. Install k6
 -  macOS: brew install k6
 -  Windows: choco install k6
 -  Linux: sudo apt-get install k6

 3. Basic k6 script structure
import http from 'k6/http';
import { check, sleep } from 'k6';


export const options = {
  vus: 10, // Virtual Users
  duration: '30s',
};

export default function () {
  const res = http.get('https://test-api.k6.io/public/crocodiles/');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

Ngày 3-4: Các hàm và options cơ bản

// script-basic.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';

// Custom metrics
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');

export const options = {
  // Stages - Tăng dần lượng users
  stages: [
    { duration: '30s', target: 20 },  // Ramp-up: 0 → 20 VUs
    { duration: '1m', target: 20 },   // Giữ ổn định
    { duration: '30s', target: 0 },   // Ramp-down: 20 → 0 VUs
  ],

  // Thresholds - Ngưỡng chấp nhận được
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% request < 500ms
    errors: ['rate<0.1'],            // Lỗi < 10%
  },
};

export default function () {
  // Group requests
  group('API Public Endpoints', function () {
    // GET request
    const res1 = http.get('https://test-api.k6.io/public/crocodiles/');

    // Check response
    const checkResult = check(res1, {
      'status is 200': (r) => r.status === 200,
      'has data': (r) => r.json().length > 0,
    });

    // Track error rate
    errorRate.add(!checkResult);

    // Track response time
    responseTime.add(res1.timings.duration);
  });

  sleep(Math.random() * 2); // Random sleep 0-2s
}

Ngày 5-7: Thực hành với API thực tế

// script-api-test.js
import http from 'k6/http';
import { check, group } from 'k6';

const BASE_URL = 'https://jsonplaceholder.typicode.com';

export const options = {
  scenarios: {
    // Scenario 1: Kiểm tra load bình thường
    normal_load: {
      executor: 'constant-vus',
      vus: 10,
      duration: '1m',
      gracefulStop: '10s',
    },
    // Scenario 2: Stress test
    spike_test: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '10s', target: 50 },
        { duration: '30s', target: 50 },
        { duration: '10s', target: 0 },
      ],
    },
  },
  thresholds: {
    'http_req_duration{scenario:normal_load}': ['p(95)<1000'],
    'http_req_duration{scenario:spike_test}': ['p(95)<2000'],
  },
};

export default function () {
  group('JSONPlaceholder API Tests', function () {
    // Test 1: Get all posts
    const postsResponse = http.get(`${BASE_URL}/posts`);
    check(postsResponse, {
      'GET /posts status 200': (r) => r.status === 200,
      'GET /posts has data': (r) => r.json().length > 0,
    });

    // Test 2: Get single post (dynamic ID)
    const postId = Math.floor(Math.random() * 100) + 1;
    const singlePostResponse = http.get(`${BASE_URL}/posts/${postId}`);
    check(singlePostResponse, {
      'GET single post status 200': (r) => r.status === 200,
      'Post has valid structure': (r) => {
        const data = r.json();
        return data.id && data.title && data.body;
      },
    });

    // Test 3: Create new post
    const payload = JSON.stringify({
      title: 'k6 Test',
      body: 'This is a test from k6',
      userId: 1,
    });

    const params = {
      headers: {
        'Content-Type': 'application/json',
      },
    };

    const createResponse = http.post(
      `${BASE_URL}/posts`,
      payload,
      params
    );

    check(createResponse, {
      'POST status 201': (r) => r.status === 201,
      'Response has ID': (r) => r.json().id !== undefined,
    });
  });
}

Tuần 2: Nâng cao & Thực hành

Ngày 8-9: Data-driven testing với CSV/JSON

// script-data-driven.js
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";

// Đọc dữ liệu từ CSV
const usersData = new SharedArray('users', function () {
  return papaparse.parse(open('./data/users.csv'), { header: true }).data;
});

// Đọc dữ liệu từ JSON
const testData = JSON.parse(open('./data/test-data.json'));

export const options = {
  vus: 5,
  duration: '1m',
};

export default function () {
  // Lấy ngẫu nhiên 1 user từ CSV
  const randomUser = usersData[Math.floor(Math.random() * usersData.length)];

  // Test login với data từ CSV
  const loginPayload = JSON.stringify({
    username: randomUser.username,
    password: randomUser.password,
  });

  const loginResponse = http.post(
    'https://test-api.k6.io/auth/token/login/',
    loginPayload,
    { headers: { 'Content-Type': 'application/json' } }
  );

  check(loginResponse, {
    'login successful': (r) => r.status === 200,
  });

  // Extract token nếu login thành công
  if (loginResponse.status === 200) {
    const token = loginResponse.json().access;

    // Gọi API protected với token
    const headers = {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    };

    // Test với data từ JSON
    testData.posts.forEach(post => {
      const postResponse = http.post(
        'https://test-api.k6.io/my/crocodiles/',
        JSON.stringify(post),
        { headers }
      );

      check(postResponse, {
        'post created': (r) => r.status === 201,
      });
    });
  }
}

// Generate HTML report
export function handleSummary(data) {
  return {
    "summary.html": htmlReport(data),
  };
}

Ngày 10-11: Authentication & Sessions

// script-authentication.js
import http from 'k6/http';
import { check, fail } from 'k6';
import { browser } from 'k6/browser';

export const options = {
  scenarios: {
    // API testing
    api: {
      executor: 'shared-iterations',
      vus: 5,
      iterations: 20,
    },
    // Browser testing (nếu cần test frontend)
    browser: {
      executor: 'per-vu-iterations',
      vus: 2,
      iterations: 5,
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
};

// Helper function để xử lý session
function handleAuth() {
  // Step 1: Get authentication token
  const loginRes = http.post(
    'https://test-api.k6.io/auth/token/login/',
    JSON.stringify({
      username: 'test_user',
      password: 'superCroc2019',
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );

  if (!check(loginRes, { 'login succeeded': (r) => r.status === 200 })) {
    fail('Authentication failed');
  }

  const authToken = loginRes.json().access;

  // Step 2: Use token in subsequent requests
  return {
    headers: {
      'Authorization': `Bearer ${authToken}`,
      'Content-Type': 'application/json',
    },
  };
}

// API test với authentication
export default function () {
  const authHeaders = handleAuth();

  // Get user profile
  const profileRes = http.get(
    'https://test-api.k6.io/my/profile/',
    authHeaders
  );

  check(profileRes, {
    'profile retrieved': (r) => r.status === 200,
  });

  // Create resource
  const createRes = http.post(
    'https://test-api.k6.io/my/crocodiles/',
    JSON.stringify({
      name: `Croc_${__VU}_${__ITER}`,
      sex: 'M',
      date_of_birth: '2001-01-01',
    }),
    authHeaders
  );

  check(createRes, {
    'resource created': (r) => r.status === 201,
  });
}

// Browser testing (cho frontend)
export function browserTest() {
  const page = browser.newPage();

  try {
    page.goto('https://test.k6.io/');

    // Take screenshot
    page.screenshot({ path: `screenshot_${__VU}.png` });

    // Fill login form
    page.locator('input[name="login"]').type('admin');
    page.locator('input[name="password"]').type('123');
    page.locator('button[type="submit"]').click();

    // Wait for navigation
    page.waitForNavigation();

    // Verify login success
    check(page, {
      'login success': () => page.url().includes('welcome'),
    });
  } finally {
    page.close();
  }
}

Ngày 12-14: Performance Dashboard & CI/CD

# .github/workflows/k6-tests.yml
name: k6 Load Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * *'  # Chạy hàng ngày 2AM

jobs:
  load-test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup k6
      uses: grafana/setup-k6-action@v1

    - name: Run k6 tests
      run: |
        k6 run \
          --out json=results.json \
          --summary-export=summary.json \
          scripts/api-test.js

    - name: Upload results
      uses: actions/upload-artifact@v3
      with:
        name: k6-results
        path: results.json

    - name: Generate HTML Report
      run: |
        npm install -g k6-summary
        k6-summary summary.json --output report.html

    - name: Deploy to GitHub Pages
      uses: JamesIves/github-pages-deploy-action@v4
      with:
        folder: reports

Tuần 3: Real-world Scenarios

Ngày 15-16: E-commerce Load Test

// script-ecommerce.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Trend, Rate, Counter } from 'k6/metrics';

// Custom metrics
const addToCartTime = new Trend('add_to_cart_time');
const checkoutTime = new Trend('checkout_time');
const conversionRate = new Rate('conversion_rate');
const totalOrders = new Counter('total_orders');

const BASE_URL = 'https://demo-api.k6.io';

export const options = {
  scenarios: {
    browse_products: {
      executor: 'constant-vus',
      vus: 20,
      duration: '5m',
      exec: 'browseProducts',
    },
    add_to_cart: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 10 },
        { duration: '3m', target: 10 },
        { duration: '1m', target: 0 },
      ],
      exec: 'addToCart',
    },
    checkout: {
      executor: 'constant-arrival-rate',
      rate: 2, // 2 requests per second
      timeUnit: '1s',
      duration: '3m',
      preAllocatedVUs: 5,
      exec: 'checkout',
    },
  },
  thresholds: {
    'http_req_duration{scenario:browse_products}': ['p(95)<1000'],
    'http_req_duration{scenario:checkout}': ['p(95)<2000'],
    'conversion_rate': ['rate>0.3'], // 30% conversion rate
  },
};

// Scenario 1: Browse products
export function browseProducts() {
  group('Browse Catalog', () => {
    // View homepage
    const homeRes = http.get(`${BASE_URL}/`);
    check(homeRes, { 'homepage loaded': (r) => r.status === 200 });

    // Browse categories
    const categoriesRes = http.get(`${BASE_URL}/categories`);
    check(categoriesRes, {
      'categories loaded': (r) => r.status === 200,
      'has categories': (r) => r.json().length > 0,
    });

    // View products in category
    const categoryId = Math.floor(Math.random() * 5) + 1;
    const productsRes = http.get(
      `${BASE_URL}/categories/${categoryId}/products`
    );
    check(productsRes, {
      'products loaded': (r) => r.status === 200,
    });

    // View product detail
    if (productsRes.json().length > 0) {
      const product = productsRes.json()[0];
      const productDetailRes = http.get(
        `${BASE_URL}/products/${product.id}`
      );
      check(productDetailRes, {
        'product detail loaded': (r) => r.status === 200,
      });
    }
  });

  sleep(Math.random() * 3 + 1);
}

// Scenario 2: Add to cart
export function addToCart() {
  group('Shopping Cart', () => {
    // Login
    const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
      username: `user_${__VU}`,
      password: 'password123',
    }));

    const authToken = loginRes.json().token;
    const headers = {
      'Authorization': `Bearer ${authToken}`,
      'Content-Type': 'application/json',
    };

    // Add item to cart
    const startTime = Date.now();
    const addRes = http.post(
      `${BASE_URL}/cart/items`,
      JSON.stringify({
        productId: Math.floor(Math.random() * 100) + 1,
        quantity: Math.floor(Math.random() * 3) + 1,
      }),
      { headers }
    );

    const duration = Date.now() - startTime;
    addToCartTime.add(duration);

    check(addRes, {
      'item added to cart': (r) => r.status === 201,
    });
  });

  sleep(Math.random() * 2);
}

// Scenario 3: Checkout process
export function checkout() {
  group('Checkout Process', () => {
    const startTime = Date.now();

    // 1. Get cart
    const cartRes = http.get(`${BASE_URL}/cart`);
    check(cartRes, { 'cart retrieved': (r) => r.status === 200 });

    // 2. Create order
    const orderRes = http.post(
      `${BASE_URL}/orders`,
      JSON.stringify({
        cartId: cartRes.json().id,
        shippingAddress: {
          street: '123 Test St',
          city: 'Test City',
          zipCode: '12345',
        },
      })
    );

    // 3. Process payment
    if (orderRes.status === 201) {
      const paymentRes = http.post(
        `${BASE_URL}/payments`,
        JSON.stringify({
          orderId: orderRes.json().id,
          paymentMethod: 'credit_card',
          cardNumber: '4111111111111111',
        })
      );

      const duration = Date.now() - startTime;
      checkoutTime.add(duration);

      if (paymentRes.status === 200) {
        conversionRate.add(1);
        totalOrders.add(1);
      }

      check(paymentRes, {
        'payment successful': (r) => r.status === 200,
      });
    }
  });

  sleep(1);
}

Ngày 17-18: WebSocket & GraphQL Testing

// script-websocket-graphql.js
import ws from 'k6/ws';
import http from 'k6/http';
import { check } from 'k6';

// WebSocket Test
export function websocketTest() {
  const url = 'wss://echo.websocket.org';

  ws.connect(url, {}, function (socket) {
    socket.on('open', function open() {
      console.log('WebSocket connected');

      // Send message
      socket.send('Hello from k6');

      // Send multiple messages
      for (let i = 0; i < 5; i++) {
        socket.send(`Message ${i}`);
      }
    });

    socket.on('message', function (message) {
      console.log(`Received: ${message}`);

      check(message, {
        'message is correct': (msg) => msg.includes('Message'),
      });
    });

    socket.on('close', function () {
      console.log('WebSocket disconnected');
    });

    socket.setTimeout(function () {
      console.log('Closing socket after 10 seconds');
      socket.close();
    }, 10000);
  });
}

// GraphQL Test
export function graphqlTest() {
  const url = 'https://graphqlzero.almansi.me/api';

  // GraphQL query
  const query = `
    query ($id: ID!) {
      user(id: $id) {
        id
        name
        username
        email
        address {
          street
          city
        }
      }
    }
  `;

  const variables = {
    id: 1,
  };

  const headers = {
    'Content-Type': 'application/json',
  };

  const payload = JSON.stringify({
    query,
    variables,
  });

  const res = http.post(url, payload, { headers });

  check(res, {
    'GraphQL response status 200': (r) => r.status === 200,
    'has user data': (r) => {
      const data = r.json();
      return data.data && data.data.user;
    },
    'user has correct id': (r) => r.json().data.user.id === 1,
  });
}

export const options = {
  vus: 5,
  duration: '1m',
};

export default function () {
  // Run both tests
  websocketTest();
  graphqlTest();
}

Tuần 4: Optimization & Production

Ngày 19-20: Monitoring & Alerting

// script-monitoring.js
import http from 'k6/http';
import { check, group } from 'k6';
import { Counter, Rate, Trend } from 'k6/metrics';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js';

// Custom metrics cho monitoring
const successCounter = new Counter('successful_requests');
const errorCounter = new Counter('failed_requests');
const errorRate = new Rate('error_rate');
const percentile95 = new Trend('percentile_95');
const percentile99 = new Trend('percentile_99');

export const options = {
  scenarios: {
    smoke: {
      executor: 'constant-vus',
      vus: 1,
      duration: '1m',
      tags: { test_type: 'smoke' },
    },
    load: {
      executor: 'ramping-vus',
      stages: [
        { duration: '2m', target: 50 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 0 },
      ],
      tags: { test_type: 'load' },
    },
  },

  thresholds: {
    // Global thresholds
    'http_req_duration': ['p(95)<500', 'p(99)<1000'],
    'http_req_failed': ['rate<0.01'],

    // Tag-based thresholds
    'http_req_duration{test_type:smoke}': ['max<1000'],
    'http_req_duration{test_type:load}': ['p(95)<800'],

    // Custom metric thresholds
    'error_rate': ['rate<0.05'],
  },

  // Output to multiple destinations
  ext: {
    loadimpact: {
      projectID: 123456,
      name: 'Production Load Test',
    },
  },
};

// Helper function để ghi log
function logRequest(method, url, status, duration) {
  console.log(
    JSON.stringify({
      timestamp: new Date().toISOString(),
      vu: __VU,
      iter: __ITER,
      method,
      url,
      status,
      duration,
    })
  );
}

export default function () {
  group('Health Checks', () => {
    // Health check endpoint
    const healthRes = http.get('https://test-api.k6.io/health');

    const healthCheck = check(healthRes, {
      'health check passed': (r) => r.status === 200,
    });

    if (healthCheck) {
      successCounter.add(1);
    } else {
      errorCounter.add(1);
      errorRate.add(1);
    }

    logRequest(
      'GET',
      'https://test-api.k6.io/health',
      healthRes.status,
      healthRes.timings.duration
    );
  });

  group('API Performance', () => {
    const start = new Date();
    const res = http.batch([
      ['GET', 'https://test-api.k6.io/public/crocodiles/'],
      ['GET', 'https://test-api.k6.io/public/crocodiles/1/'],
    ]);

    const duration = new Date() - start;

    // Track percentiles
    percentile95.add(duration);
    percentile99.add(duration);

    check(res[0], {
      'batch request 1 OK': (r) => r.status === 200,
    });

    check(res[1], {
      'batch request 2 OK': (r) => r.status === 200,
    });
  });
}

// Custom summary handler
export function handleSummary(data) {
  // In ra console
  console.log(textSummary(data));

  // Lưu ra file JSON
  const resultFile = `results/${Date.now()}_results.json`;

  return {
    'stdout': textSummary(data, { indent: ' ', enableColors: true }),
    [resultFile]: JSON.stringify(data),
  };
}

Ngày 21-22: Distributed Testing với k6 Cloud

// script-distributed.js
import http from 'k6/http';
import { check } from 'k6';
import exec from 'k6/execution';

// Distributed test configuration
export const options = {
  // Khi chạy local
  // vus: 10,
  // duration: '30s',

  // Khi chạy với k6 cloud
  cloud: {
    // distribution: {
    //   'loadZone1': { loadZone: 'amazon:us:ashburn', percent: 50 },
    //   'loadZone2': { loadZone: 'amazon:eu:frankfurt', percent: 50 },
    // },
    projectID: 12345,
    name: 'Global Load Test',
  },

  // Multiple executors cho complex scenarios
  scenarios: {
    europe_users: {
      executor: 'constant-vus',
      vus: 50,
      duration: '5m',
      tags: { region: 'europe' },
      env: { BASE_URL: 'https://eu-api.example.com' },
    },
    us_users: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },
        { duration: '3m', target: 100 },
        { duration: '1m', target: 0 },
      ],
      tags: { region: 'us' },
      env: { BASE_URL: 'https://us-api.example.com' },
    },
  },

  // Advanced thresholds
  thresholds: {
    // Global
    'http_req_duration': ['p(95)<1000'],

    // Per region
    'http_req_duration{region:us}': ['p(95)<800'],
    'http_req_duration{region:europe}': ['p(95)<1200'],

    // Per scenario
    'http_reqs{scenario:europe_users}': ['count>10000'],
    'http_reqs{scenario:us_users}': ['count>20000'],
  },
};

// Lấy environment variable
const BASE_URL = __ENV.BASE_URL || 'https://test-api.k6.io';

export default function () {
  // Thêm tag cho request
  const tags = {
    vu_id: __VU,
    iter: __ITER,
    region: exec.scenario.tags.region || 'unknown',
  };

  const res = http.get(
    `${BASE_URL}/public/crocodiles/`,
    { tags }
  );

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time acceptable': (r) => r.timings.duration < 2000,
  }, tags);

  // Logic khác nhau theo region
  if (tags.region === 'europe') {
    // European specific logic
    http.get(`${BASE_URL}/eu-specific-endpoint/`, { tags });
  } else if (tags.region === 'us') {
    // US specific logic
    http.get(`${BASE_URL}/us-specific-endpoint/`, { tags });
  }
}

Ngày 23-24: Best Practices & Optimization

// best-practices.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

// ========== BEST PRACTICES ==========

// 1. Sử dụng environment variables
const API_URL = __ENV.API_URL || 'https://test-api.k6.io';
const VUS = parseInt(__ENV.VUS) || 10;

// 2. Shared data để giảm memory usage
import { SharedArray } from 'k6/data';
const testData = new SharedArray('test data', function () {
  return JSON.parse(open('./data/large-data.json'));
});

// 3. Custom metrics cho business logic
const businessErrorRate = new Rate('business_errors');

// 4. Tags cho filtering
const tags = {
  service: 'api-gateway',
  environment: __ENV.ENV || 'staging',
  test_id: __ENV.TEST_ID || 'default',
};

export const options = {
  // 5. Đúng executor cho use case
  scenarios: {
    // Dùng cho API testing
    api_load: {
      executor: 'constant-vus',
      vus: VUS,
      duration: '5m',
      tags: { ...tags, test_type: 'api' },
    },
    // Dùng cho spike testing
    spike: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 100 },
        { duration: '1m', target: 100 },
        { duration: '30s', target: 0 },
      ],
      tags: { ...tags, test_type: 'spike' },
    },
  },

  // 6. Realistic thresholds
  thresholds: {
    // Loại bỏ các threshold không cần thiết
    'http_req_duration{test_type:api}': ['p(95)<500'],
    'http_req_duration{test_type:spike}': ['p(95)<1000'],
    'business_errors': ['rate<0.01'],
  },

  // 7. System resource limits
  // systemTags: ['status', 'method', 'url', 'name'],
  discardResponseBodies: true, // Giảm memory khi không cần response body
};

// 8. Modular test functions
function login() {
  const res = http.post(`${API_URL}/auth/login`, JSON.stringify({
    username: `test_${__VU}`,
    password: 'password',
  }));

  check(res, {
    'login success': (r) => r.status === 200,
  });

  return res.json().token;
}

function getUserProfile(token) {
  const headers = {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  };

  const res = http.get(`${API_URL}/user/profile`, { headers });

  // Business logic validation
  const isBusinessError = res.json().status === 'error';
  businessErrorRate.add(isBusinessError);

  return res;
}

function performAction(token, action) {
  const headers = {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  };

  const payload = testData[Math.floor(Math.random() * testData.length)];

  const res = http.post(
    `${API_URL}/actions/${action}`,
    JSON.stringify(payload),
    { headers }
  );

  // Validate business rules
  check(res, {
    [`${action} successful`]: (r) => r.status === 200,
    'valid response format': (r) => {
      try {
        const data = r.json();
        return data.id && data.timestamp;
      } catch (e) {
        return false;
      }
    },
  });

  return res;
}

// 9. Main execution flow
export default function () {
  // Phase 1: Authentication
  const token = login();
  if (!token) {
    return; // Skip nếu login fail
  }

  sleep(0.5);

  // Phase 2: User actions
  const profile = getUserProfile(token);
  if (profile.status !== 200) {
    return;
  }

  sleep(Math.random() * 1);

  // Phase 3: Business actions
  const actions = ['create', 'update', 'delete'];
  const randomAction = actions[Math.floor(Math.random() * actions.length)];
  performAction(token, randomAction);

  // 10. Think time thực tế
  sleep(Math.random() * 2 + 1);
}

Ngày 25-28: Project tổng hợp

// project-complete.js
/**
 * COMPLETE E-COMMERCE LOAD TEST SUITE
 * Kết hợp tất cả kiến thức đã học
 */

import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';
import { Trend, Rate, Counter, Gauge } from 'k6/metrics';
import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js";
import exec from 'k6/execution';

// ========== CUSTOM METRICS ==========
const productBrowseTime = new Trend('product_browse_time');
const cartOperationTime = new Trend('cart_operation_time');
const checkoutTime = new Trend('checkout_time');
const conversionRate = new Rate('conversion_rate');
const activeUsers = new Gauge('active_users');
const totalRevenue = new Counter('total_revenue');

// ========== CONFIGURATION ==========
const BASE_URL = __ENV.BASE_URL || 'https://demo-store.k6.io';
const TEST_DURATION = __ENV.TEST_DURATION || '10m';
const TARGET_VUS = parseInt(__ENV.TARGET_VUS) || 50;

export const options = {
  scenarios: {
    // 1. Browsing behavior (70% users)
    browsing: {
      executor: 'constant-vus',
      vus: Math.floor(TARGET_VUS * 0.7),
      duration: TEST_DURATION,
      exec: 'browseScenario',
      tags: { user_type: 'browser' },
    },

    // 2. Buying behavior (20% users)
    buying: {
      executor: 'ramping-vus',
      startVUs: Math.floor(TARGET_VUS * 0.2),
      stages: [
        { duration: '2m', target: Math.floor(TARGET_VUS * 0.2) },
        { duration: '6m', target: Math.floor(TARGET_VUS * 0.2) },
        { duration: '2m', target: 0 },
      ],
      exec: 'buyScenario',
      tags: { user_type: 'buyer' },
    },

    // 3. Search behavior (10% users)
    searching: {
      executor: 'per-vu-iterations',
      vus: Math.floor(TARGET_VUS * 0.1),
      iterations: 20,
      exec: 'searchScenario',
      tags: { user_type: 'searcher' },
    },
  },

  thresholds: {
    // Performance thresholds
    'http_req_duration{user_type:browser}': ['p(95)<1000'],
    'http_req_duration{user_type:buyer}': ['p(95)<2000'],
    'http_req_duration{user_type:searcher}': ['p(95)<1500'],

    // Business thresholds
    'conversion_rate': ['rate>0.2'], // 20% conversion rate
    'product_browse_time': ['p(95)<800'],
    'checkout_time': ['p(95)<3000'],

    // Error thresholds
    'http_req_failed': ['rate<0.01'],
  },

  // Tags for filtering
  tags: {
    project: 'ecommerce-load-test',
    environment: __ENV.ENV || 'staging',
    version: '1.0.0',
  },
};

// ========== DATA ==========
const products = [
  { id: 1, name: 'Laptop', price: 999.99 },
  { id: 2, name: 'Phone', price: 699.99 },
  { id: 3, name: 'Tablet', price: 499.99 },
  { id: 4, name: 'Headphones', price: 199.99 },
  { id: 5, name: 'Smartwatch', price: 299.99 },
];

const users = new Array(100).fill(0).map((_, i) => ({
  username: `user${i + 1}`,
  email: `user${i + 1}@test.com`,
  password: 'Test123!',
}));

// ========== HELPER FUNCTIONS ==========
function getRandomUser() {
  return users[Math.floor(Math.random() * users.length)];
}

function getRandomProduct() {
  return products[Math.floor(Math.random() * products.length)];
}

function simulateThinkTime(min, max) {
  sleep(Math.random() * (max - min) + min);
}

// ========== AUTH MODULE ==========
function login(user) {
  const start = Date.now();

  const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
    username: user.username,
    password: user.password,
  }), {
    headers: { 'Content-Type': 'application/json' },
    tags: { action: 'login' },
  });

  const duration = Date.now() - start;

  check(res, {
    'login successful': (r) => r.status === 200,
    'received auth token': (r) => r.json().token !== undefined,
  });

  if (res.status !== 200) {
    fail('Login failed');
  }

  return {
    token: res.json().token,
    duration,
  };
}

// ========== SCENARIOS ==========
export function browseScenario() {
  activeUsers.add(1);

  group('Browse Products', () => {
    // Homepage
    const homeRes = http.get(`${BASE_URL}/`, {
      tags: { page: 'home' },
    });
    check(homeRes, { 'homepage loaded': (r) => r.status === 200 });

    simulateThinkTime(2, 5);

    // Browse categories
    const categories = ['electronics', 'clothing', 'books'];
    const category = categories[Math.floor(Math.random() * categories.length)];

    const start = Date.now();
    const categoryRes = http.get(`${BASE_URL}/api/categories/${category}/products`, {
      tags: { page: 'category', category },
    });
    productBrowseTime.add(Date.now() - start);

    check(categoryRes, {
      'category products loaded': (r) => r.status === 200,
    });

    simulateThinkTime(1, 3);

    // View product detail
    if (categoryRes.json().length > 0) {
      const product = categoryRes.json()[0];
      http.get(`${BASE_URL}/api/products/${product.id}`, {
        tags: { page: 'product_detail', product_id: product.id },
      });
    }
  });

  activeUsers.add(-1);
}

export function buyScenario() {
  const user = getRandomUser();

  group('Complete Purchase Flow', () => {
    // 1. Login
    const auth = login(user);

    simulateThinkTime(1, 2);

    // 2. Browse and add to cart
    const product = getRandomProduct();

    const cartStart = Date.now();
    const cartRes = http.post(
      `${BASE_URL}/api/cart/items`,
      JSON.stringify({
        productId: product.id,
        quantity: Math.floor(Math.random() * 3) + 1,
      }),
      {
        headers: {
          'Authorization': `Bearer ${auth.token}`,
          'Content-Type': 'application/json',
        },
        tags: { action: 'add_to_cart' },
      }
    );
    cartOperationTime.add(Date.now() - cartStart);

    check(cartRes, {
      'item added to cart': (r) => r.status === 201,
    });

    simulateThinkTime(2, 4);

    // 3. Checkout (30% of buyers will checkout)
    if (Math.random() < 0.3) {
      group('Checkout Process', () => {
        const checkoutStart = Date.now();

        // Create order
        const orderRes = http.post(
          `${BASE_URL}/api/orders`,
          JSON.stringify({
            cartId: cartRes.json().cartId,
            shippingAddress: {
              street: '123 Test St',
              city: 'Test City',
              zipCode: '12345',
            },
          }),
          {
            headers: {
              'Authorization': `Bearer ${auth.token}`,
              'Content-Type': 'application/json',
            },
            tags: { action: 'create_order' },
          }
        );

        if (orderRes.status === 201) {
          // Process payment
          const paymentRes = http.post(
            `${BASE_URL}/api/payments`,
            JSON.stringify({
              orderId: orderRes.json().orderId,
              amount: product.price,
              paymentMethod: 'credit_card',
            }),
            {
              headers: {
                'Authorization': `Bearer ${auth.token}`,
                'Content-Type': 'application/json',
              },
              tags: { action: 'process_payment' },
            }
          );

          const checkoutDuration = Date.now() - checkoutStart;
          checkoutTime.add(checkoutDuration);

          if (paymentRes.status === 200) {
            conversionRate.add(1);
            totalRevenue.add(product.price);

            check(paymentRes, {
              'payment successful': (r) => r.status === 200,
              'order completed': (r) => r.json().status === 'completed',
            });
          }
        }
      });
    }
  });
}

export function searchScenario() {
  group('Search and Filter', () => {
    const searchTerms = ['laptop', 'phone', 'gadget', 'wireless', 'smart'];
    const term = searchTerms[Math.floor(Math.random() * searchTerms.length)];

    // Search
    const searchRes = http.get(
      `${BASE_URL}/api/search?q=${term}&sort=price&order=asc`,
      { tags: { action: 'search', term } }
    );

    check(searchRes, {
      'search successful': (r) => r.status === 200,
      'search returned results': (r) => r.json().results.length > 0,
    });

    simulateThinkTime(1, 3);

    // Apply filters
    if (searchRes.json().results.length > 0) {
      http.get(
        `${BASE_URL}/api/search?q=${term}&minPrice=100&maxPrice=500&inStock=true`,
        { tags: { action: 'filter' } }
      );
    }
  });
}

// ========== MAIN EXECUTION ==========
export default function () {
  // This won't be called because we use specific scenario executors
  console.log('Main execution - should not be called');
}

// ========== REPORTING ==========
export function handleSummary(data) {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');

  // Business metrics summary
  const businessSummary = {
    'Total Revenue': `$${data.metrics.total_revenue.values.count.toFixed(2)}`,
    'Conversion Rate': `${(data.metrics.conversion_rate.values.rate * 100).toFixed(1)}%`,
    'Active Users (avg)': data.metrics.active_users.values.avg.toFixed(1),
    'Total Orders': data.metrics.total_revenue.values.count,
  };

  console.log('=== BUSINESS SUMMARY ===');
  Object.entries(businessSummary).forEach(([key, value]) => {
    console.log(`${key}: ${value}`);
  });

  return {
    // HTML Report
    [`reports/${timestamp}_report.html`]: htmlReport(data),

    // JSON data for analysis
    [`reports/${timestamp}_data.json`]: JSON.stringify(data),

    // Text summary
    'stdout': textSummary(data, {
      indent: '  ',
      enableColors: true,
    }),
  };
}

📁 Cấu trúc project hoàn chỉnh

k6-project/
│
├── scripts/
│   ├── 01-basic/
│   │   ├── simple-test.js
│   │   └── basic-options.js
│   ├── 02-intermediate/
│   │   ├── data-driven.js
│   │   ├── authentication.js
│   │   └── scenarios.js
│   ├── 03-advanced/
│   │   ├── ecommerce.js
│   │   ├── websocket-graphql.js
│   │   └── distributed.js
│   └── 04-production/
│       ├── monitoring.js
│       ├── complete-project.js
│       └── best-practices.js
│
├── data/
│   ├── users.csv
│   ├── products.json
│   └── test-data.json
│
├── config/
│   ├── staging.json
│   ├── production.json
│   └── local.json
│
├── reports/
│   └── (auto-generated)
│
├── package.json
├── docker-compose.yml
├── .github/workflows/
│   └── k6-tests.yml
│
├── README.md
└── .env.example

🎯 Mẹo học hiệu quả

Tuần 1-2: Làm quen với syntax, chạy các script đơn giản

Tuần 3: Áp dụng vào dự án thật của bạn

Tuần 4: Tối ưu và đưa vào CI/CD

Thực hành hàng ngày: Chạy ít nhất 1 test mỗi ngày

Document lại: Ghi chép các vấn đề gặp phải và cách giải quyết

Virtual Users (VUs)

Simulate concurrent users.

Execute the default function in a loop.

✔ Stages / Scenarios

Stages → simple ramping pattern.

Scenarios → multiple types of traffic, each with its own executor.

✔ Metrics

Business metrics → revenue, orders, users.

System metrics → response time, error rate, throughput.

✔ Thresholds

Business thresholds → revenue, orders, users.

System thresholds → response time, error rate, throughput.

✔ Executors (critical for interviews)

Be able to explain when you'd use each:

Executor What it does When it's used constant-vus Fixed number of VUs Steady load tests ramping-vus Gradually increase/decrease VUs Stress tests / Soak tests constant-arrival-rate Fixed RPS, not tied to VUs API throughput tests shared-iterations Shared fixed number of iterations Quick smoke/perf checks per-vu-iterations Each VU runs X iterations Flows with deterministic paths