backend/test/doc/AI_GUIDE.md

477 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 測試架構 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/)
---
**記住**:此架構的核心是**模組化**和**可重複使用**。每個場景應該獨立、可測試,並且可以輕鬆組合形成更複雜的流程。