上下文传递

This commit is contained in:
longpeng 2025-06-27 01:01:58 +08:00
parent 4dcbef8307
commit eba56bc756
8 changed files with 1209 additions and 22 deletions

View File

@ -1,2 +0,0 @@
# Beacon

View File

@ -0,0 +1,255 @@
# Activity 之间参数传递指南
## 概述
在动态测试工作流中,多个 Activity 之间经常需要传递参数。例如,第二个 Activity 可能需要使用第一个 Activity 响应的某个字段作为入参。本文档详细说明了如何实现这种参数传递机制。
## 支持的变量格式
### 1. 全局变量
格式:`${global.key}`
- 用于访问工作流级别的全局变量
- 这些变量在工作流启动时通过 `GlobalParameters` 传入
### 2. 步骤结果变量
格式:`${step.stepId.field}`
- 用于访问之前步骤的执行结果
- `stepId` 是步骤的ID
- `field` 是结果中的具体字段
## 变量引用示例
### API 测试结果变量
```json
{
"test_case_id": "api_test_001",
"endpoint": "/api/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"testuser\", \"password\": \"testpass\"}",
"expected_status_code": 200
}
```
假设这个API测试的步骤ID是123执行后返回
```json
{
"base_result": {
"success": true,
"message": "API Test Passed"
},
"actual_status_code": 200,
"response_body": "{\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\", \"user_id\": 456}",
"headers": {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json"
}
}
```
### 可用的变量引用
1. **响应体**`${step.123.response_body}`
2. **状态码**`${step.123.actual_status_code}`
3. **响应头**`${step.123.headers.Authorization}`
4. **JSON字段**`${step.123.json_token}` (自动提取的token字段)
5. **用户ID**`${step.123.json_user_id}` (自动提取的user_id字段)
## 实际使用场景
### 场景1登录后使用Token进行API调用
**步骤1登录API**
```json
{
"test_case_id": "login_test",
"endpoint": "/api/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"testuser\", \"password\": \"testpass\"}",
"expected_status_code": 200
}
```
**步骤2使用Token调用受保护的API**
```json
{
"test_case_id": "protected_api_test",
"endpoint": "/api/user/profile",
"http_method": "GET",
"headers": {
"Authorization": "${step.123.json_token}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
### 场景2使用响应头中的Token
```json
{
"test_case_id": "api_with_header_token",
"endpoint": "/api/data",
"http_method": "GET",
"headers": {
"Authorization": "${step.123.headers.Authorization}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
### 场景3使用响应体中的用户ID
```json
{
"test_case_id": "user_data_test",
"endpoint": "/api/users/${step.123.json_user_id}/data",
"http_method": "GET",
"headers": {
"Authorization": "${step.123.json_token}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
## UI 测试结果变量
### UI 测试结果示例
```json
{
"base_result": {
"success": true,
"message": "UI Test Passed"
},
"screenshot_url": "https://s3.amazonaws.com/screenshots/test_001.png",
"html_report_url": "https://s3.amazonaws.com/reports/test_001.html"
}
```
### 可用的变量引用
1. **截图URL**`${step.456.screenshot_url}`
2. **报告URL**`${step.456.html_report_url}`
## 全局变量使用
### 工作流启动时传入全局变量
```go
input := &pb.DynamicTestRunInput{
RunId: "test_run_001",
CompositeCaseId: "composite_case_001",
GlobalParameters: map[string]string{
"base_url": "https://api.example.com",
"environment": "staging",
"api_version": "v1",
},
}
```
### 在步骤参数中使用全局变量
```json
{
"test_case_id": "api_test",
"endpoint": "${global.base_url}/${global.api_version}/users",
"http_method": "GET",
"headers": {
"X-Environment": "${global.environment}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
## 复杂场景示例
### 多步骤参数传递链
**步骤1用户注册**
```json
{
"test_case_id": "user_registration",
"endpoint": "/api/register",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"newuser\", \"email\": \"newuser@example.com\", \"password\": \"password123\"}",
"expected_status_code": 201
}
```
**步骤2用户登录使用注册返回的用户ID**
```json
{
"test_case_id": "user_login",
"endpoint": "/api/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"request_body": "{\"username\": \"newuser\", \"password\": \"password123\"}",
"expected_status_code": 200
}
```
**步骤3获取用户信息使用登录返回的token**
```json
{
"test_case_id": "get_user_info",
"endpoint": "/api/users/${step.123.json_user_id}",
"http_method": "GET",
"headers": {
"Authorization": "Bearer ${step.124.json_token}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}
```
**步骤4更新用户信息使用步骤3的token和用户ID**
```json
{
"test_case_id": "update_user_info",
"endpoint": "/api/users/${step.123.json_user_id}",
"http_method": "PUT",
"headers": {
"Authorization": "Bearer ${step.124.json_token}",
"Content-Type": "application/json"
},
"request_body": "{\"display_name\": \"Updated User\", \"bio\": \"This is my updated bio\"}",
"expected_status_code": 200
}
```
## 注意事项
1. **变量解析顺序**:先解析全局变量,再解析步骤变量
2. **错误处理**:如果变量不存在,会保持原始占位符不变
3. **类型安全**:所有变量都会被转换为字符串
4. **性能考虑**:变量解析在每次步骤执行时进行,避免频繁的字符串操作
5. **调试支持**:工作流日志会记录变量解析过程
## 最佳实践
1. **使用有意义的变量名**:避免使用过于简单的变量名
2. **文档化变量**:在步骤描述中说明使用的变量
3. **测试变量解析**:在开发环境中测试变量解析是否正确
4. **错误处理**:在步骤参数中提供默认值或错误处理逻辑
5. **版本控制**:记录变量格式的变更,确保向后兼容
## 扩展功能
### 自定义变量提取器
可以通过扩展 `extractApiResultToGlobalVariables``extractUiResultToGlobalVariables` 函数来支持更多自定义字段的提取。
### 条件变量
可以基于步骤的成功/失败状态来设置不同的变量值。
### 变量验证
可以在变量解析时添加验证逻辑,确保必需的变量存在且格式正确。

View File

@ -18,7 +18,7 @@ func NewWorkflowHandler() *WorkflowHandler {
// StartWorkflow POST /api/workflow/start
func (h *WorkflowHandler) StartWorkflow(c *gin.Context) {
err := h.service.Start("11")
err := h.service.Start("13")
if err != nil {
c.JSON(500, gin.H{
"code": -1,

View File

@ -4,6 +4,7 @@ import (
"beacon/models"
"beacon/pkg/dao/mysql"
"beacon/utils"
"encoding/json"
"fmt"
"go.uber.org/zap"
)
@ -63,7 +64,7 @@ func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCa
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
ParametersJson: stepReq.ParametersJson,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
}
steps = append(steps, step)
@ -187,7 +188,7 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
ParametersJson: stepReq.ParametersJson,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
}
steps = append(steps, step)
@ -213,6 +214,28 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
return s.GetCompositeCaseByID(id)
}
func fixParametersJson(jsonStr string) string {
var params map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &params); err != nil {
// Not a valid json string, return as is.
return jsonStr
}
if rb, ok := params["request_body"]; ok {
if rbs, ok := rb.(string); ok && rbs == "{" {
params["request_body"] = "{}"
}
}
fixedJSONBytes, err := json.Marshal(params)
if err != nil {
// Failed to marshal back, return original.
return jsonStr
}
return string(fixedJSONBytes)
}
// DeleteCompositeCase 删除复合案例
func (s *CompositeCaseService) DeleteCompositeCase(id uint) error {
zap.L().Info("开始删除复合案例", zap.Uint("id", id))

View File

@ -0,0 +1,484 @@
package utils
import (
"encoding/json"
"fmt"
"go.uber.org/zap"
"regexp"
"strconv"
"strings"
"beacon/pkg/pb"
)
// ParameterProcessor 参数处理器用于处理activity之间的参数传递
type ParameterProcessor struct {
globalVariables map[string]interface{}
activityResults map[int64]*ActivityResult
}
// ActivityResult 用于存储每个activity的执行结果
type ActivityResult struct {
StepID int64 `json:"step_id"`
StepName string `json:"step_name"`
Success bool `json:"success"`
Data interface{} `json:"data"`
}
// NewParameterProcessor 创建新的参数处理器
func NewParameterProcessor(globalVariables map[string]interface{}) *ParameterProcessor {
return &ParameterProcessor{
globalVariables: globalVariables,
activityResults: make(map[int64]*ActivityResult),
}
}
// AddActivityResult 添加activity执行结果
func (p *ParameterProcessor) AddActivityResult(stepID int64, result *ActivityResult) {
p.activityResults[stepID] = result
}
// ProcessTemplate 处理参数模板,替换其中的变量引用
func (p *ParameterProcessor) ProcessTemplate(template string) (string, error) {
if template == "" {
return template, nil
}
result := template
// 1. 替换全局变量引用 ${global.key}
result = p.replaceGlobalVariables(result)
// 2. 替换步骤结果变量引用 ${step.stepId.field}
result = p.replaceStepVariables(result)
// 3. 替换条件变量引用 ${if.condition:value1:value2}
result = p.replaceConditionalVariables(result)
// 4. 替换函数调用 ${func:arg1:arg2}
result = p.replaceFunctionCalls(result)
return result, nil
}
// replaceGlobalVariables 替换全局变量引用
func (p *ParameterProcessor) replaceGlobalVariables(template string) string {
// 使用正则表达式匹配 ${global.key} 格式
re := regexp.MustCompile(`\$\{global\.([^}]+)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
// 提取变量名
matches := re.FindStringSubmatch(match)
if len(matches) < 2 {
return match // 保持原样
}
key := matches[1]
if value, exists := p.globalVariables[key]; exists {
return fmt.Sprintf("%v", value)
}
return match // 变量不存在,保持原样
})
}
// replaceStepVariables 替换步骤结果变量引用
func (p *ParameterProcessor) replaceStepVariables(template string) string {
// 匹配 ${step.stepId.field} 格式
re := regexp.MustCompile(`\$\{step\.(\d+)\.([^}]+)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) < 3 {
return match
}
stepIDStr := matches[1]
field := matches[2]
stepID, err := strconv.ParseInt(stepIDStr, 10, 64)
if err != nil {
return match
}
result, exists := p.activityResults[stepID]
if !exists {
return match
}
return p.extractFieldValue(result, field)
})
}
// replaceConditionalVariables 替换条件变量引用
func (p *ParameterProcessor) replaceConditionalVariables(template string) string {
// 匹配 ${if.condition:value1:value2} 格式
re := regexp.MustCompile(`\$\{if\.([^:]+):([^:]*):([^}]*)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) < 4 {
return match
}
condition := matches[1]
valueIfTrue := matches[2]
valueIfFalse := matches[3]
if p.evaluateCondition(condition) {
return valueIfTrue
}
return valueIfFalse
})
}
// replaceFunctionCalls 替换函数调用
func (p *ParameterProcessor) replaceFunctionCalls(template string) string {
// 匹配 ${func:arg1:arg2} 格式
re := regexp.MustCompile(`\$\{([^:]+):([^}]*)\}`)
return re.ReplaceAllStringFunc(template, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) < 3 {
return match
}
funcName := matches[1]
args := matches[2]
return p.executeFunction(funcName, args)
})
}
// extractFieldValue 从activity结果中提取指定字段的值
func (p *ParameterProcessor) extractFieldValue(result *ActivityResult, field string) string {
switch result.Data.(type) {
case *pb.ApiTestResult:
return p.extractApiResultField(result.Data.(*pb.ApiTestResult), field)
case *pb.UiTestResult:
return p.extractUiResultField(result.Data.(*pb.UiTestResult), field)
default:
return ""
}
}
// extractApiResultField 从API测试结果中提取字段值
func (p *ParameterProcessor) extractApiResultField(result *pb.ApiTestResult, field string) string {
switch field {
case "response_body":
// 保证输出是 JSON 字符串格式
b, err := json.Marshal(result.ResponseBody)
if err == nil {
return string(b[1 : len(b)-1]) // 去掉外层引号
}
return result.ResponseBody
case "actual_status_code":
return strconv.Itoa(int(result.ActualStatusCode))
case "success":
return strconv.FormatBool(result.BaseResult.Success)
case "message":
return result.BaseResult.Message
default:
zap.L().Debug("未知字段", zap.String("field", field), zap.String("ResponseBody", result.ResponseBody))
// 检查是否是响应头
if strings.HasPrefix(field, "headers.") {
headerKey := strings.TrimPrefix(field, "headers.")
if headerValue, exists := result.Headers[headerKey]; exists {
return headerValue
}
}
// 检查是否是JSON字段
if strings.HasPrefix(field, "json.") {
jsonField := strings.TrimPrefix(field, "json.")
return p.extractJsonField(result.ResponseBody, jsonField)
}
return ""
}
}
// extractUiResultField 从UI测试结果中提取字段值
func (p *ParameterProcessor) extractUiResultField(result *pb.UiTestResult, field string) string {
switch field {
case "screenshot_url":
return result.ScreenshotUrl
case "html_report_url":
return result.HtmlReportUrl
case "success":
return strconv.FormatBool(result.BaseResult.Success)
case "message":
return result.BaseResult.Message
default:
return ""
}
}
// extractJsonField 从JSON响应体中提取指定字段
func (p *ParameterProcessor) extractJsonField(responseBody, field string) string {
if responseBody == "" {
return ""
}
var data map[string]interface{}
if err := json.Unmarshal([]byte(responseBody), &data); err != nil {
return ""
}
// 支持嵌套字段,如 "user.id"
keys := strings.Split(field, ".")
current := data
for i, key := range keys {
if i == len(keys)-1 {
// 最后一个键
if value, exists := current[key]; exists {
return fmt.Sprintf("%v", value)
}
} else {
// 中间键,需要继续深入
if next, exists := current[key]; exists {
if nextMap, ok := next.(map[string]interface{}); ok {
current = nextMap
} else {
return ""
}
} else {
return ""
}
}
}
return ""
}
// evaluateCondition 评估条件表达式
func (p *ParameterProcessor) evaluateCondition(condition string) bool {
// 支持简单的条件表达式
// 例如step.123.success, global.environment == "production"
// 检查步骤成功状态
if strings.HasSuffix(condition, ".success") {
stepIDStr := strings.TrimSuffix(condition, ".success")
if strings.HasPrefix(stepIDStr, "step.") {
stepIDStr = strings.TrimPrefix(stepIDStr, "step.")
stepID, err := strconv.ParseInt(stepIDStr, 10, 64)
if err == nil {
if result, exists := p.activityResults[stepID]; exists {
return result.Success
}
}
}
}
// 检查全局变量
if strings.HasPrefix(condition, "global.") {
key := strings.TrimPrefix(condition, "global.")
if value, exists := p.globalVariables[key]; exists {
// 转换为布尔值
switch v := value.(type) {
case bool:
return v
case string:
return v != "" && v != "false" && v != "0"
case int, int64, float64:
return fmt.Sprintf("%v", v) != "0"
default:
return value != nil
}
}
}
// 检查相等性
if strings.Contains(condition, "==") {
parts := strings.Split(condition, "==")
if len(parts) == 2 {
left := strings.TrimSpace(parts[0])
right := strings.TrimSpace(strings.Trim(parts[1], `"'`))
// 检查全局变量
if strings.HasPrefix(left, "global.") {
key := strings.TrimPrefix(left, "global.")
if value, exists := p.globalVariables[key]; exists {
return fmt.Sprintf("%v", value) == right
}
}
}
}
return false
}
// executeFunction 执行函数调用
func (p *ParameterProcessor) executeFunction(funcName, args string) string {
argList := strings.Split(args, ":")
switch funcName {
case "concat":
// 连接字符串
return strings.Join(argList, "")
case "upper":
// 转换为大写
if len(argList) > 0 {
return strings.ToUpper(argList[0])
}
case "lower":
// 转换为小写
if len(argList) > 0 {
return strings.ToLower(argList[0])
}
case "substring":
// 子字符串
if len(argList) >= 3 {
str := argList[0]
start, _ := strconv.Atoi(argList[1])
end, _ := strconv.Atoi(argList[2])
if start < len(str) && end <= len(str) && start < end {
return str[start:end]
}
}
case "default":
// 默认值
if len(argList) >= 2 {
value := argList[0]
defaultValue := argList[1]
if value == "" {
return defaultValue
}
return value
}
}
return ""
}
// GetAvailableVariables 获取所有可用的变量列表
func (p *ParameterProcessor) GetAvailableVariables() map[string][]string {
variables := make(map[string][]string)
// 全局变量
var globalVars []string
for key := range p.globalVariables {
globalVars = append(globalVars, key)
}
variables["global"] = globalVars
// 步骤变量
for stepID, result := range p.activityResults {
stepKey := fmt.Sprintf("step.%d", stepID)
var stepVars []string
switch result.Data.(type) {
case *pb.ApiTestResult:
stepVars = []string{
"response_body",
"actual_status_code",
"success",
"message",
"headers.*",
"json.*",
}
case *pb.UiTestResult:
stepVars = []string{
"screenshot_url",
"html_report_url",
"success",
"message",
}
}
variables[stepKey] = stepVars
}
return variables
}
// ValidateTemplate 验证模板中的变量引用是否有效
func (p *ParameterProcessor) ValidateTemplate(template string) (bool, []string) {
var errors []string
// 检查全局变量引用
globalRe := regexp.MustCompile(`\$\{global\.([^}]+)\}`)
matches := globalRe.FindAllStringSubmatch(template, -1)
for _, match := range matches {
if len(match) >= 2 {
key := match[1]
if _, exists := p.globalVariables[key]; !exists {
errors = append(errors, fmt.Sprintf("全局变量 '%s' 不存在", key))
}
}
}
// 检查步骤变量引用
stepRe := regexp.MustCompile(`\$\{step\.(\d+)\.([^}]+)\}`)
matches = stepRe.FindAllStringSubmatch(template, -1)
for _, match := range matches {
if len(match) >= 3 {
stepIDStr := match[1]
field := match[2]
stepID, err := strconv.ParseInt(stepIDStr, 10, 64)
if err != nil {
errors = append(errors, fmt.Sprintf("无效的步骤ID: %s", stepIDStr))
continue
}
if _, exists := p.activityResults[stepID]; !exists {
errors = append(errors, fmt.Sprintf("步骤 %d 不存在", stepID))
continue
}
// 检查字段是否有效
if !p.isValidField(stepID, field) {
errors = append(errors, fmt.Sprintf("步骤 %d 不支持字段: %s", stepID, field))
}
}
}
return len(errors) == 0, errors
}
// isValidField 检查字段是否有效
func (p *ParameterProcessor) isValidField(stepID int64, field string) bool {
result, exists := p.activityResults[stepID]
if !exists {
return false
}
switch result.Data.(type) {
case *pb.ApiTestResult:
validFields := map[string]bool{
"response_body": true,
"actual_status_code": true,
"success": true,
"message": true,
}
if validFields[field] {
return true
}
// 检查是否是响应头
if strings.HasPrefix(field, "headers.") {
return true
}
// 检查是否是JSON字段
if strings.HasPrefix(field, "json.") {
return true
}
case *pb.UiTestResult:
validFields := map[string]bool{
"screenshot_url": true,
"html_report_url": true,
"success": true,
"message": true,
}
return validFields[field]
}
return false
}

View File

@ -0,0 +1,385 @@
package utils
import (
"beacon/pkg/pb"
"testing"
)
func TestParameterProcessor_ProcessTemplate(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "staging",
"api_version": "v1",
"timeout": "30",
"retry_count": "3",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 模拟API测试结果
apiResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "API Test Passed",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user_id": 456, "name": "John Doe"}`,
Headers: map[string]string{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json",
},
}
// 添加activity结果
processor.AddActivityResult(123, &ActivityResult{
StepID: 123,
StepName: "RunApiTest",
Success: true,
Data: apiResult,
})
// 测试用例
tests := []struct {
name string
template string
expected string
}{
{
name: "全局变量替换",
template: `{"endpoint": "${global.base_url}/${global.api_version}/users"}`,
expected: `{"endpoint": "https://api.example.com/v1/users"}`,
},
{
name: "API响应体替换",
template: `{"token": "${step.123.response_body}"}`,
expected: `{"token": "{\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\", \"user_id\": 456, \"name\": \"John Doe\"}"}`,
},
{
name: "API状态码替换",
template: `{"status": "${step.123.actual_status_code}"}`,
expected: `{"status": "200"}`,
},
{
name: "API响应头替换",
template: `{"auth": "${step.123.headers.Authorization}"}`,
expected: `{"auth": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}`,
},
{
name: "JSON字段提取",
template: `{"user_id": "${step.123.json.user_id}", "name": "${step.123.json.name}"}`,
expected: `{"user_id": "456", "name": "John Doe"}`,
},
{
name: "混合变量替换",
template: `{"endpoint": "${global.base_url}/${global.api_version}/users/${step.123.json.user_id}", "headers": {"Authorization": "${step.123.json.token}"}}`,
expected: `{"endpoint": "https://api.example.com/v1/users/456", "headers": {"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}`,
},
{
name: "条件变量替换",
template: `{"env": "${if.step.123.success:production:staging}"}`,
expected: `{"env": "production"}`,
},
{
name: "函数调用",
template: `{"upper_name": "${upper:${step.123.json.name}}"}`,
expected: `{"upper_name": "JOHN DOE"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := processor.ProcessTemplate(tt.template)
if err != nil {
t.Errorf("ProcessTemplate() error = %v", err)
return
}
if result != tt.expected {
t.Errorf("ProcessTemplate() = %v, want %v", result, tt.expected)
}
})
}
}
func TestParameterProcessor_ValidateTemplate(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "staging",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 添加一个activity结果
apiResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "API Test Passed",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "test-token"}`,
Headers: map[string]string{
"Authorization": "Bearer test-token",
},
}
processor.AddActivityResult(123, &ActivityResult{
StepID: 123,
StepName: "RunApiTest",
Success: true,
Data: apiResult,
})
// 测试用例
tests := []struct {
name string
template string
expectValid bool
expectErrors int
}{
{
name: "有效模板",
template: `{"endpoint": "${global.base_url}/users", "token": "${step.123.json.token}"}`,
expectValid: true,
expectErrors: 0,
},
{
name: "无效的全局变量",
template: `{"endpoint": "${global.invalid_key}/users"}`,
expectValid: false,
expectErrors: 1,
},
{
name: "无效的步骤ID",
template: `{"token": "${step.999.json.token}"}`,
expectValid: false,
expectErrors: 1,
},
{
name: "无效的字段",
template: `{"invalid": "${step.123.invalid_field}"}`,
expectValid: false,
expectErrors: 1,
},
{
name: "多个错误",
template: `{"endpoint": "${global.invalid_key}/users", "token": "${step.999.json.token}"}`,
expectValid: false,
expectErrors: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid, errors := processor.ValidateTemplate(tt.template)
if isValid != tt.expectValid {
t.Errorf("ValidateTemplate() isValid = %v, want %v", isValid, tt.expectValid)
}
if len(errors) != tt.expectErrors {
t.Errorf("ValidateTemplate() errors count = %d, want %d", len(errors), tt.expectErrors)
}
})
}
}
func TestParameterProcessor_GetAvailableVariables(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "staging",
"api_version": "v1",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 添加API测试结果
apiResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "API Test Passed",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "test-token"}`,
Headers: map[string]string{
"Authorization": "Bearer test-token",
},
}
processor.AddActivityResult(123, &ActivityResult{
StepID: 123,
StepName: "RunApiTest",
Success: true,
Data: apiResult,
})
// 添加UI测试结果
uiResult := &pb.UiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "UI Test Passed",
},
ScreenshotUrl: "https://s3.amazonaws.com/screenshots/test.png",
HtmlReportUrl: "https://s3.amazonaws.com/reports/test.html",
}
processor.AddActivityResult(456, &ActivityResult{
StepID: 456,
StepName: "RunUiTest",
Success: true,
Data: uiResult,
})
// 获取可用变量
variables := processor.GetAvailableVariables()
// 验证全局变量
if globalVars, exists := variables["global"]; !exists {
t.Error("Global variables not found")
} else {
expectedGlobalVars := []string{"base_url", "environment", "api_version"}
for _, expected := range expectedGlobalVars {
found := false
for _, actual := range globalVars {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Global variable '%s' not found", expected)
}
}
}
// 验证步骤变量
if step123Vars, exists := variables["step.123"]; !exists {
t.Error("Step 123 variables not found")
} else {
expectedStepVars := []string{"response_body", "actual_status_code", "success", "message", "headers.*", "json.*"}
for _, expected := range expectedStepVars {
found := false
for _, actual := range step123Vars {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Step 123 variable '%s' not found", expected)
}
}
}
if step456Vars, exists := variables["step.456"]; !exists {
t.Error("Step 456 variables not found")
} else {
expectedStepVars := []string{"screenshot_url", "html_report_url", "success", "message"}
for _, expected := range expectedStepVars {
found := false
for _, actual := range step456Vars {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Step 456 variable '%s' not found", expected)
}
}
}
}
func TestParameterProcessor_ComplexScenarios(t *testing.T) {
// 创建测试用的全局变量
globalVariables := map[string]interface{}{
"base_url": "https://api.example.com",
"environment": "production",
"api_version": "v2",
"timeout": "60",
}
// 创建参数处理器
processor := NewParameterProcessor(globalVariables)
// 模拟用户注册结果
registerResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "User registered successfully",
},
ActualStatusCode: 201,
ResponseBody: `{"user_id": 789, "username": "newuser", "email": "newuser@example.com"}`,
Headers: map[string]string{
"Content-Type": "application/json",
},
}
// 模拟用户登录结果
loginResult := &pb.ApiTestResult{
BaseResult: &pb.BaseTestResult{
Success: true,
Message: "User logged in successfully",
},
ActualStatusCode: 200,
ResponseBody: `{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "refresh123", "expires_in": 3600}`,
Headers: map[string]string{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json",
},
}
// 添加activity结果
processor.AddActivityResult(201, &ActivityResult{
StepID: 201,
StepName: "RunApiTest",
Success: true,
Data: registerResult,
})
processor.AddActivityResult(202, &ActivityResult{
StepID: 202,
StepName: "RunApiTest",
Success: true,
Data: loginResult,
})
// 测试复杂的参数传递场景
complexTemplate := `{
"test_case_id": "get_user_profile",
"endpoint": "${global.base_url}/${global.api_version}/users/${step.201.json.user_id}/profile",
"http_method": "GET",
"headers": {
"Authorization": "Bearer ${step.202.json.token}",
"X-Environment": "${global.environment}",
"X-Timeout": "${global.timeout}",
"Content-Type": "application/json"
},
"expected_status_code": 200
}`
expected := `{
"test_case_id": "get_user_profile",
"endpoint": "https://api.example.com/v2/users/789/profile",
"http_method": "GET",
"headers": {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"X-Environment": "production",
"X-Timeout": "60",
"Content-Type": "application/json"
},
"expected_status_code": 200
}`
result, err := processor.ProcessTemplate(complexTemplate)
if err != nil {
t.Errorf("ProcessTemplate() error = %v", err)
return
}
if result != expected {
t.Errorf("ProcessTemplate() = %v, want %v", result, expected)
}
}

View File

@ -10,7 +10,7 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he
实际执行API测试的函数
可以集成 pytest, requests 等库
"""
base_url = "http://101.89.127.197:9080" # 假设 API 服务的基地址
base_url = "" # 假设 API 服务的基地址
full_url = f"{base_url}{endpoint}"
log_output = []
@ -25,7 +25,12 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he
if http_method.upper() == "GET":
response = requests.get(full_url, headers=headers, timeout=10)
elif http_method.upper() == "POST":
json_data = json.loads(request_body) if isinstance(request_body, str) else request_body
print(request_body)
if request_body == "":
json_data = "{}"
else:
# 如果 request_body 是字符串,尝试将其解析为 JSON
json_data = json.loads(request_body) if isinstance(request_body, str) else request_body
response = requests.post(full_url, headers=headers, data=json_data, timeout=10)
# ... 其他 HTTP 方法
else:

View File

@ -9,6 +9,7 @@ import (
"beacon/activities" // 假设你的 activity 包在这个路径
"beacon/pkg/pb" // 你的 Protobuf 路径
"beacon/utils" // 导入参数处理器
"go.temporal.io/sdk/workflow"
// 假设你有数据库访问层Workflow 不能直接访问 DB需要通过 Activity 或预加载数据
// "your_module_path/go-server/dal" // 例如 Data Access Layer
@ -78,6 +79,19 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转
)
// 初始化全局变量,包含输入参数中的全局参数
globalVariables := make(map[string]interface{})
if input.GlobalParameters != nil {
for key, value := range input.GlobalParameters {
globalVariables[key] = value
}
}
// 创建参数处理器
parameterProcessor := utils.NewParameterProcessor(globalVariables)
logger.Info("Initialized global variables", "variables", globalVariables)
// ========================================================================================
// 步骤4: 主执行循环 - 动态执行各个测试步骤
// ========================================================================================
@ -87,30 +101,45 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
logger.Info("Executing step", "stepOrder", step.StepOrder, "activityName", step.ActivityName)
// ------------------------------------------------------------------------------------
// 步骤4.1: 动态构造 Activity 输入参数
// 步骤4.1: 动态构造 Activity 输入参数(支持参数模板解析)
// ------------------------------------------------------------------------------------
// 因为不同的 Activity 需要不同类型的输入参数,这里使用工厂模式
// 根据 activity_name 决定使用哪个 Protobuf 结构来反序列化 JSON 参数
var activityInput interface{} // 存储特定 Activity 的输入参数(类型由 activity_name 决定)
var activityResult interface{} // 存储特定 Activity 的输出结果(类型由 activity_name 决定)
// 使用参数处理器解析参数模板,替换其中的变量引用
processedParametersJson, err := parameterProcessor.ProcessTemplate(step.ParametersJson)
if err != nil {
logger.Error("Failed to process parameter template", "error", err, "stepOrder", step.StepOrder)
overallSuccess = false
break
}
// 验证模板中的变量引用是否有效
if isValid, errors := parameterProcessor.ValidateTemplate(step.ParametersJson); !isValid {
logger.Error("Parameter template validation failed", "errors", errors, "stepOrder", step.StepOrder)
overallSuccess = false
break
}
// 根据不同的 Activity 类型,将 JSON 字符串参数反序列化为对应的 Protobuf 结构
// 这是动态工作流的核心:同一个工作流可以执行不同类型的测试
switch step.ActivityName {
case "RunApiTest":
apiReq := &pb.ApiTestRequest{}
// 1. 首先验证JSON格式
if !json.Valid([]byte(step.ParametersJson)) {
if !json.Valid([]byte(processedParametersJson)) {
logger.Error("Invalid JSON format in parameters")
overallSuccess = false
break
}
// 2. 解析JSON时增加错误详情
if err := json.Unmarshal([]byte(step.ParametersJson), apiReq); err != nil {
if err := json.Unmarshal([]byte(processedParametersJson), apiReq); err != nil {
logger.Error("Failed to unmarshal API test parameters",
"error", err,
"raw_json", step.ParametersJson)
"raw_json", processedParametersJson)
overallSuccess = false
break
}
@ -121,22 +150,13 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
overallSuccess = false
break
}
/*// API 测试:创建 API 测试请求结构并解析参数
logger.Info("Running api test activity", "ParametersJson", step.ParametersJson)
fmt.Println(reflect.TypeOf(step.ParametersJson))
apiReq := &pb.ApiTestRequest{}
if err := json.Unmarshal([]byte(step.ParametersJson), apiReq); err != nil {
logger.Error("Failed to unmarshal API test parameters", "error", err)
overallSuccess = false
break // 参数解析失败,跳出当前步骤
}*/
activityInput = apiReq
activityResult = &pb.ApiTestResult{} // 预创建结果容器
case "RunUiTest":
// UI 测试:创建 UI 测试请求结构并解析参数
uiReq := &pb.UiTestRequest{}
if err := json.Unmarshal([]byte(step.ParametersJson), uiReq); err != nil {
if err := json.Unmarshal([]byte(processedParametersJson), uiReq); err != nil {
logger.Error("Failed to unmarshal UI test parameters", "error", err)
overallSuccess = false
break // 参数解析失败,跳出当前步骤
@ -149,7 +169,7 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
// TODO: 实现环境准备的参数解析
/*
prepReq := &pb.PrepareEnvRequest{}
if err := json.Unmarshal([]byte(step.ParametersJson), prepReq); err != nil {
if err := json.Unmarshal([]byte(processedParametersJson), prepReq); err != nil {
logger.Error("Failed to unmarshal prepare env parameters", "error", err)
overallSuccess = false
break
@ -215,12 +235,29 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
stepPassed = false
overallSuccess = false
}
// 存储activity结果用于参数传递
activityResult := &utils.ActivityResult{
StepID: step.StepId,
StepName: step.ActivityName,
Success: res.BaseResult.Success,
Data: res,
}
parameterProcessor.AddActivityResult(step.StepId, activityResult)
case *pb.UiTestResult:
uiResults = append(uiResults, res) // 收集 UI 测试结果
if !res.BaseResult.Success { // 检查业务逻辑是否成功
stepPassed = false
overallSuccess = false
}
// 存储activity结果用于参数传递
activityResult := &utils.ActivityResult{
StepID: step.StepId,
StepName: step.ActivityName,
Success: res.BaseResult.Success,
Data: res,
}
parameterProcessor.AddActivityResult(step.StepId, activityResult)
// 可以在这里添加更多结果类型的处理
}
logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed)