# 測試架構 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`(訂單相關): ```javascript /** * 訂單相關 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 需要組合使用,創建流程場景: ```javascript /** * 訂單流程端到端場景 */ 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`) ```javascript 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`) ```javascript 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`) ```javascript 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!'; // ... 測試邏輯 } ``` ## 🔑 關鍵模式 ### 場景函數模板 ```javascript 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, // 其他有用的數據 }; } ``` ### 測試文件模板 ```javascript 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` 參數,包含 `baseUrl` 和 `customMetrics` - [ ] 為請求添加 `tags`,包含 `name`, `api`, `method` - [ ] 使用 `check()` 驗證響應結果 - [ ] 支援可選的自定義指標 - [ ] 返回結構化的結果對象 - [ ] 如果需要,在 `scenarios/e2e/` 創建流程場景 - [ ] 在 `tests/smoke/` 創建冒煙測試 - [ ] 在 `tests/pre/` 創建負載/壓力測試 - [ ] 在 `tests/prod/` 創建夜間測試(如果適用) - [ ] 所有測試文件使用適當的 `scenarios` 配置 - [ ] 設置適當的 `thresholds` ## 🚀 快速開始 1. **查看現有場景**:參考 `scenarios/apis/auth.js` 或 `scenarios/apis/user.js` 2. **複製模板**:使用上面的場景函數模板 3. **調整參數**:根據新 API 的需求調整 4. **創建測試**:在對應的 `tests/` 目錄下創建測試文件 5. **運行測試**:使用 k6 運行測試文件 ## 📚 參考資源 - [k6 官方文檔](https://k6.io/docs/) - [k6 Scenarios](https://k6.io/docs/using-k6/scenarios/) - [k6 Thresholds](https://k6.io/docs/using-k6/thresholds/) - [k6 Tags](https://k6.io/docs/using-k6/tags-and-groups/) --- **記住**:此架構的核心是**模組化**和**可重複使用**。每個場景應該獨立、可測試,並且可以輕鬆組合形成更複雜的流程。