backend/test/doc/AI_GUIDE.md

12 KiB
Raw Permalink Blame History

測試架構 AI 指南

本文檔旨在幫助 AI 助手理解此測試架構的設計原則和使用方式,以便在添加新 API 時能夠快速套用相同的模式。

📁 目錄結構

test/
├── scenarios/           # 可重複使用的場景模組
│   ├── apis/          # API 層級場景(單一 API 端點)
│   │   ├── auth.js    # 認證相關場景
│   │   ├── user.js    # 使用者相關場景
│   │   └── health.js  # 健康檢查場景
│   └── e2e/           # 端到端業務流程場景
│       ├── authentication-flow.js  # 認證流程
│       └── user-profile-flow.js     # 使用者資料流程
├── tests/             # 不同環境的測試配置
│   ├── smoke/        # 冒煙測試Dev/QA 環境)
│   ├── pre/          # 預發布環境測試(負載/壓力測試)
│   └── prod/         # 生產環境測試(夜間監控)
└── AI_GUIDE.md       # 本文件

🎯 設計原則

1. 模組化場景設計

  • 場景模組化:每個 API 端點對應一個場景函數,可獨立使用
  • 可擴展預設行為:場景函數接受 options 參數,可覆蓋預設行為
  • 避免緊密耦合:場景邏輯與測試配置分離

2. 自定義指標(可選)

  • 場景函數支援可選的 customMetrics 參數
  • 如果不提供,使用預設指標
  • 如果需要特殊指標,可以傳入自定義指標對象

3. 請求標籤與檢查

  • 每個請求都添加 tags,便於在 k6 中過濾和分析
  • 使用 check() 函數檢查請求結果
  • 檢查項目包括:狀態碼、響應結構、業務邏輯驗證

4. 使用 Scenarios 設置工作負載

  • 測試文件使用 k6 的 scenarios 配置工作負載
  • 不同環境使用不同的 executor 和配置
  • 避免只使用 Groups使用 Scenarios 提供更大靈活性

5. 避免多用途測試

  • 每個測試文件專注於一個主要目的
  • 每個環境一個測試文件
  • 這樣可以避免混合責任,並有助於追踪歷史結果

📝 如何添加新 API 場景

步驟 1: 在 scenarios/apis/ 創建或更新場景模組

假設要添加一個新的 API 模組 order.js(訂單相關):

/**
 * 訂單相關 API 場景模組
 */
import http from 'k6/http';
import { check } from 'k6';
import { Rate, Trend } from 'k6/metrics';

// 可選的自定義指標
const createOrderSuccessRate = new Rate('order_create_success');
const createOrderDuration = new Trend('order_create_duration');

/**
 * 創建訂單
 * @param {Object} options - 配置選項
 * @param {string} options.baseUrl - API 基礎 URL
 * @param {string} options.accessToken - Access Token
 * @param {Object} options.orderData - 訂單資料
 * @param {Object} options.customMetrics - 自定義指標對象(可選)
 * @returns {Object} 創建結果
 */
export function createOrder(options = {}) {
  const {
    baseUrl = __ENV.BASE_URL || 'https://localhost:8888',
    accessToken,
    orderData = {},
    customMetrics = null,
  } = options;

  if (!accessToken) {
    throw new Error('accessToken is required');
  }

  const url = `${baseUrl}/api/v1/orders`;
  const payload = JSON.stringify(orderData);

  const params = {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    tags: {
      name: 'order_create',
      api: 'order',
      method: 'create_order',
    },
  };

  const startTime = Date.now();
  const res = http.post(url, payload, params);
  const duration = Date.now() - startTime;

  const success = check(res, {
    'create order status is 200': (r) => r.status === 200,
    'create order has order_id': (r) => {
      try {
        const body = JSON.parse(r.body);
        return body.order_id && body.order_id.length > 0;
      } catch {
        return false;
      }
    },
  }, { name: 'create_order_checks' });

  if (customMetrics) {
    customMetrics.createOrderSuccessRate?.add(success);
    customMetrics.createOrderDuration?.add(duration);
  } else {
    createOrderSuccessRate.add(success);
    createOrderDuration.add(duration);
  }

  let result = null;
  if (res.status === 200) {
    try {
      result = JSON.parse(res.body);
    } catch (e) {
      console.error('Failed to parse create order response:', e);
    }
  }

  return {
    success,
    status: res.status,
    response: result,
  };
}

步驟 2: 在 scenarios/e2e/ 創建流程場景(如果需要)

如果有多個 API 需要組合使用,創建流程場景:

/**
 * 訂單流程端到端場景
 */
import * as order from '../apis/order.js';
import { sleep } from 'k6';

/**
 * 完整訂單流程(創建 → 查詢 → 取消)
 */
export function orderLifecycleFlow(options = {}) {
  const {
    baseUrl = __ENV.BASE_URL || 'https://localhost:8888',
    accessToken,
    orderData = {},
  } = options;

  // 步驟 1: 創建訂單
  const createResult = order.createOrder({
    baseUrl,
    accessToken,
    orderData,
  });

  if (!createResult.success || !createResult.response) {
    return {
      success: false,
      step: 'create',
      error: 'Create order failed',
      createResult,
    };
  }

  sleep(1);

  // 步驟 2: 查詢訂單
  const orderId = createResult.response.order_id;
  const getResult = order.getOrder({
    baseUrl,
    accessToken,
    orderId,
  });

  // ... 其他步驟

  return {
    success: true,
    createResult,
    getResult,
  };
}

步驟 3: 在 tests/ 創建測試文件

3.1 冒煙測試 (tests/smoke/smoke-order-test.js)

import { createOrder } from '../../scenarios/apis/order.js';
import { loginWithCredentials } from '../../scenarios/apis/auth.js';

export const options = {
  scenarios: {
    smoke_order: {
      executor: 'shared-iterations',
      vus: 1,
      iterations: 1,
      maxDuration: '30s',
      tags: { test_type: 'smoke', api: 'order' },
    },
  },
  thresholds: {
    checks: ['rate==1.0'],
    http_req_duration: ['p(95)<2000'],
  },
};

export default function () {
  const baseUrl = __ENV.BASE_URL || 'https://localhost:8888';
  
  // 1. 登入獲取 Token
  const loginResult = loginWithCredentials({
    baseUrl,
    loginId: 'test@example.com',
    password: 'Test123!',
  });

  if (!loginResult.success || !loginResult.tokens) {
    return;
  }

  // 2. 創建訂單
  createOrder({
    baseUrl,
    accessToken: loginResult.tokens.accessToken,
    orderData: { /* ... */ },
  });
}

3.2 負載測試 (tests/pre/load-order-test.js)

import { createOrder } from '../../scenarios/apis/order.js';
import { loginWithCredentials } from '../../scenarios/apis/auth.js';

export const options = {
  scenarios: {
    load_order: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 10 },
        { duration: '1m', target: 10 },
        { duration: '30s', target: 20 },
        { duration: '1m', target: 20 },
        { duration: '30s', target: 0 },
      ],
      gracefulRampDown: '30s',
      tags: { test_type: 'load', api: 'order', environment: 'pre' },
    },
  },
  thresholds: {
    checks: ['rate>0.95'],
    http_req_duration: ['p(95)<2000'],
    http_req_failed: ['rate<0.05'],
  },
};

export default function () {
  // ... 測試邏輯
}

3.3 生產環境測試 (tests/prod/nightly-order-test.js)

import { createOrder } from '../../scenarios/apis/order.js';
import { loginWithCredentials } from '../../scenarios/apis/auth.js';

export const options = {
  scenarios: {
    nightly_order: {
      executor: 'constant-vus',
      vus: 5,
      duration: '5m',
      tags: { test_type: 'nightly', api: 'order', environment: 'prod' },
    },
  },
  thresholds: {
    checks: ['rate>0.98'],
    http_req_duration: ['p(95)<2000'],
    http_req_failed: ['rate<0.02'],
  },
};

export default function () {
  // 注意:生產環境使用預先創建的測試帳號
  const loginId = __ENV.TEST_LOGIN_ID || 'test@example.com';
  const password = __ENV.TEST_PASSWORD || 'TestPassword123!';
  
  // ... 測試邏輯
}

🔑 關鍵模式

場景函數模板

export function apiFunctionName(options = {}) {
  const {
    baseUrl = __ENV.BASE_URL || 'https://localhost:8888',
    // 必需參數
    requiredParam,
    // 可選參數
    optionalParam = defaultValue,
    // 自定義指標(可選)
    customMetrics = null,
  } = options;

  // 參數驗證
  if (!requiredParam) {
    throw new Error('requiredParam is required');
  }

  // 構建請求
  const url = `${baseUrl}/api/v1/endpoint`;
  const payload = JSON.stringify({ /* ... */ });

  const params = {
    headers: {
      'Content-Type': 'application/json',
      // 如果需要認證
      'Authorization': `Bearer ${accessToken}`,
    },
    tags: {
      name: 'api_function_name',
      api: 'api_name',
      method: 'method_name',
    },
  };

  // 發送請求
  const startTime = Date.now();
  const res = http.post(url, payload, params); // 或 get, put, delete
  const duration = Date.now() - startTime;

  // 檢查結果
  const success = check(res, {
    'status is 200': (r) => r.status === 200,
    'has required field': (r) => {
      try {
        const body = JSON.parse(r.body);
        return body.required_field !== undefined;
      } catch {
        return false;
      }
    },
  }, { name: 'api_function_checks' });

  // 使用指標
  if (customMetrics) {
    customMetrics.successRate?.add(success);
    customMetrics.duration?.add(duration);
  } else {
    // 使用預設指標
  }

  // 解析響應
  let result = null;
  if (res.status === 200) {
    try {
      result = JSON.parse(res.body);
    } catch (e) {
      console.error('Failed to parse response:', e);
    }
  }

  // 返回結果
  return {
    success,
    status: res.status,
    response: result,
    // 其他有用的數據
  };
}

測試文件模板

import { apiFunction } from '../../scenarios/apis/api-module.js';

export const options = {
  scenarios: {
    test_name: {
      executor: 'executor_type', // shared-iterations, ramping-vus, constant-vus, etc.
      // executor 特定配置
      tags: { test_type: 'smoke|load|stress|nightly', api: 'api_name', environment: 'dev|pre|prod' },
    },
  },
  thresholds: {
    checks: ['rate>0.95'], // 根據環境調整
    http_req_duration: ['p(95)<2000'],
    http_req_failed: ['rate<0.05'],
  },
};

export default function () {
  const baseUrl = __ENV.BASE_URL || 'https://localhost:8888';
  
  // 測試邏輯
  const result = apiFunction({
    baseUrl,
    // 參數
  });
}

📊 環境配置

不同環境的測試配置

環境 測試類型 Executor VUs 持續時間 成功率要求
Dev/QA Smoke shared-iterations 1 30s 100%
Pre-release Load ramping-vus 0-20 5-10m 95%
Pre-release Stress ramping-vus 0-100 5-10m 90%
Production Nightly constant-vus 5 5-10m 98%

檢查清單

添加新 API 場景時,請確保:

  • scenarios/apis/ 創建場景模組
  • 場景函數接受 options 參數,包含 baseUrlcustomMetrics
  • 為請求添加 tags,包含 name, api, method
  • 使用 check() 驗證響應結果
  • 支援可選的自定義指標
  • 返回結構化的結果對象
  • 如果需要,在 scenarios/e2e/ 創建流程場景
  • tests/smoke/ 創建冒煙測試
  • tests/pre/ 創建負載/壓力測試
  • tests/prod/ 創建夜間測試(如果適用)
  • 所有測試文件使用適當的 scenarios 配置
  • 設置適當的 thresholds

🚀 快速開始

  1. 查看現有場景:參考 scenarios/apis/auth.jsscenarios/apis/user.js
  2. 複製模板:使用上面的場景函數模板
  3. 調整參數:根據新 API 的需求調整
  4. 創建測試:在對應的 tests/ 目錄下創建測試文件
  5. 運行測試:使用 k6 運行測試文件

📚 參考資源


記住:此架構的核心是模組化可重複使用。每個場景應該獨立、可測試,並且可以輕鬆組合形成更複雜的流程。