mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-06 20:32:44 +08:00
feat: add parameterization support for test cases and steps with configuration options
This commit is contained in:
14
config.go
14
config.go
@@ -88,6 +88,20 @@ func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
// WithParametersSetting sets parameters setting for current testcase.
|
||||
func (c *TConfig) WithParametersSetting(options ...ParametersOption) *TConfig {
|
||||
if c.ParametersSetting == nil {
|
||||
c.ParametersSetting = &TParamsConfig{}
|
||||
}
|
||||
|
||||
// apply all options
|
||||
for _, option := range options {
|
||||
option(c.ParametersSetting)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time config for current testcase.
|
||||
func (c *TConfig) SetThinkTime(strategy ThinkTimeStrategy, cfg interface{}, limit float64) *TConfig {
|
||||
c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit}
|
||||
|
||||
387
docs/parameters.md
Normal file
387
docs/parameters.md
Normal file
@@ -0,0 +1,387 @@
|
||||
# HttpRunner 参数化功能 (Parameters)
|
||||
|
||||
## 概述
|
||||
|
||||
HttpRunner 支持强大的**数据驱动测试**能力,允许用户在**测试用例(Testcase)**和**测试步骤(Step)**两个层级上进行参数化。这使得测试用例可以与外部数据文件解耦,实现更灵活、可维护性更高的自动化测试。
|
||||
|
||||
- **测试用例层级参数化**:对整个测试流程使用多组不同的数据重复执行。适用于需要验证完整业务流程的场景,例如使用不同用户登录并执行相同操作。
|
||||
- **测试步骤层级参数化**:仅在单个步骤内使用不同的参数重复执行。适用于需要验证单个功能点的场景,例如在搜索框中输入不同的关键词。
|
||||
|
||||
## 测试用例层级参数化 (TestCase-Level)
|
||||
|
||||
当您需要使用不同的数据集完整地运行整个测试流程时,应使用测试用例层级的参数化。
|
||||
|
||||
### 使用方法
|
||||
|
||||
通过在 `hrp.TestCase` 的 `Parameters` 字段中定义参数,并可选择使用 `WithParametersSetting` 进行策略配置。
|
||||
|
||||
```go
|
||||
// testcase_parameters_test.go
|
||||
func TestTestcaseParameters(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("测试用例层级参数化").
|
||||
WithParameters(map[string]interface{}{
|
||||
"username-password": [][]interface{}{
|
||||
{"user1", "pass1"},
|
||||
{"user2", "pass2"},
|
||||
{"user3", "pass3"},
|
||||
},
|
||||
}).
|
||||
WithParametersSetting(
|
||||
hrp.WithRandomOrder(), // 随机选择参数
|
||||
hrp.WithLimit(2), // 只执行2次
|
||||
),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("登录").
|
||||
POST("/api/login").
|
||||
WithBody(map[string]interface{}{
|
||||
"username": "$username",
|
||||
"password": "$password",
|
||||
}),
|
||||
hrp.NewStep("获取用户信息").
|
||||
GET("/api/user/info"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 执行结果
|
||||
|
||||
- 整个测试用例将运行两次。
|
||||
- 第一次运行时,`$username` 为 `user1`,`$password` 为 `pass1`。
|
||||
- 第二次运行时,`$username` 为 `user2`,`$password` 为 `pass2`。
|
||||
- 测试报告中会生成两个独立的测试结果,每个结果对应一组参数。
|
||||
|
||||
---
|
||||
|
||||
## 测试步骤层级参数化 (TestStep-Level)
|
||||
|
||||
当您只需要在一个测试流程中对某个特定步骤使用不同参数进行多次测试时,应使用测试步骤层级的参数化。
|
||||
|
||||
### 使用场景
|
||||
|
||||
- 需要对同一个操作使用不同参数进行多次测试
|
||||
- 减少重复的步骤定义代码
|
||||
- 在 UI 自动化测试中,对同一个界面操作使用不同的输入数据
|
||||
|
||||
### 核心设计
|
||||
|
||||
#### 1. 架构改动
|
||||
|
||||
##### StepConfig 结构扩展
|
||||
```go
|
||||
type StepConfig struct {
|
||||
StepName string `json:"name" yaml:"name"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` // 新增
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` // 新增
|
||||
Loops int `json:"loops,omitempty" yaml:"loops,omitempty"` // 已弃用,建议使用 Loop() 方法
|
||||
// ... 其他字段保持不变
|
||||
}
|
||||
```
|
||||
|
||||
##### TParamsConfig 参数配置
|
||||
```go
|
||||
type TParamsConfig struct {
|
||||
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"`
|
||||
Strategies map[string]IteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"`
|
||||
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Limit 和 Loops 的关系
|
||||
|
||||
**重要**:`Limit` 和 `Loops` 不是替代关系,而是可以同时存在的不同控制机制:
|
||||
|
||||
1. **Limit(总数限制)**:
|
||||
- 作用:限制参数迭代的**总执行次数**
|
||||
- 位置:`TParamsConfig.Limit`
|
||||
- 示例:`Limit = 4` 表示最多执行4次,无论有多少参数
|
||||
|
||||
2. **Loops(循环次数)**:
|
||||
- 作用:设置单个 parameter 的**循环使用次数**
|
||||
- 位置:`StepConfig.Loops`(通过 `Loop()` 方法设置)
|
||||
- 示例:`Loops = 2` 表示每个参数都循环执行2次
|
||||
|
||||
##### 执行顺序优化
|
||||
|
||||
用户期望的执行顺序(外层循环):
|
||||
```go
|
||||
// 参数:["A", "B"],Loops = 2
|
||||
// 执行顺序:A, B, A, B(先遍历所有参数,再进行下一轮循环)
|
||||
// 步骤命名:[loop_1_params_1], [loop_1_params_2], [loop_2_params_1], [loop_2_params_2]
|
||||
```
|
||||
|
||||
#### 3. 统一执行流程
|
||||
|
||||
```
|
||||
1. 收集所有参数组合
|
||||
2. 外层循环:循环次数 (1 到 loopTimes)
|
||||
3. 内层循环:遍历所有参数
|
||||
4. 执行步骤并收集结果
|
||||
```
|
||||
|
||||
### API 设计
|
||||
|
||||
#### 统一的参数配置方法
|
||||
|
||||
为了避免代码重复和保持API一致性,参数配置方法只在 `StepRequest` 中定义:
|
||||
|
||||
- `WithParameters()` - 设置步骤级参数
|
||||
- `WithParametersSetting()` - 设置参数选择策略(支持 option 模式)
|
||||
- `Loop()` - 设置循环次数
|
||||
- `WithVariables()` - 设置步骤变量
|
||||
|
||||
这些方法返回 `*StepRequest`,可以与其他步骤类型(如 Mobile、HTTP 等)配合使用。
|
||||
|
||||
#### Option API(推荐)
|
||||
|
||||
新的 option API 提供更简洁和灵活的参数配置方式:
|
||||
|
||||
```go
|
||||
// 单个 option
|
||||
hrp.NewStep("测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京"},
|
||||
}).
|
||||
WithParametersSetting(hrp.WithSequentialOrder()).
|
||||
Loop(2).
|
||||
Android()
|
||||
|
||||
// 多个 options 组合
|
||||
hrp.NewStep("测试").
|
||||
WithParameters(...).
|
||||
WithParametersSetting(
|
||||
hrp.WithRandomOrder(), // 设置随机选择顺序
|
||||
hrp.WithLimit(4), // 限制总执行次数
|
||||
hrp.WithStrategy("param1", hrp.IteratorStrategy{
|
||||
Name: "custom",
|
||||
PickOrder: "random",
|
||||
}),
|
||||
).
|
||||
Loop(2).
|
||||
Android()
|
||||
```
|
||||
|
||||
##### 可用的 Option 函数
|
||||
|
||||
###### 参数选择顺序
|
||||
- `hrp.WithSequentialOrder()` - 设置参数按顺序执行(默认)
|
||||
- `hrp.WithRandomOrder()` - 设置参数随机选择
|
||||
- `hrp.WithUniqueOrder()` - 设置参数唯一选择,避免重复
|
||||
|
||||
###### 其他配置
|
||||
- `hrp.WithLimit(limit int)` - 设置参数迭代总数限制
|
||||
- `hrp.WithStrategy(paramName string, strategy IteratorStrategy)` - 为特定参数设置策略
|
||||
|
||||
#### 正确的调用顺序
|
||||
|
||||
**重要**:参数配置必须在指定步骤类型之前调用:
|
||||
|
||||
```go
|
||||
// ✅ 推荐:使用 Loop 方法
|
||||
hrp.NewStep("搜索测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京", "重庆"},
|
||||
}).
|
||||
Loop(3). // 使用 Loop 方法
|
||||
Android().
|
||||
StartToGoal("进入搜索框,输入查询词「$query」")
|
||||
|
||||
// ✅ 推荐:使用新的 Option API
|
||||
hrp.NewStep("搜索测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京", "重庆"},
|
||||
}).
|
||||
WithParametersSetting(
|
||||
hrp.WithSequentialOrder(),
|
||||
hrp.WithLimit(3),
|
||||
).
|
||||
Android().
|
||||
StartToGoal("进入搜索框,输入查询词「$query」")
|
||||
|
||||
// ✅ 兼容:使用传统 TParamsConfig
|
||||
hrp.NewStep("搜索测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京", "重庆"},
|
||||
}).
|
||||
WithParametersSetting(&hrp.TParamsConfig{
|
||||
PickOrder: "sequential",
|
||||
Limit: 3,
|
||||
}).
|
||||
Android().
|
||||
StartToGoal("进入搜索框,输入查询词「$query」")
|
||||
|
||||
// ❌ 错误:在指定步骤类型后无法配置参数
|
||||
hrp.NewStep("搜索测试").
|
||||
Android().
|
||||
WithParameters(...) // 这里会编译错误
|
||||
```
|
||||
|
||||
### 基本用法
|
||||
|
||||
#### 1. 简单参数列表
|
||||
|
||||
```go
|
||||
hrp.NewStep("搜索查询词").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京", "重庆"},
|
||||
}).
|
||||
Android().
|
||||
StartToGoal("进入搜索框,输入 query「$query」,等待加载出 sug 提示词")
|
||||
```
|
||||
|
||||
#### 2. 复合参数
|
||||
|
||||
```go
|
||||
hrp.NewStep("登录测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"username-password": [][]interface{}{
|
||||
{"user1", "pass1"},
|
||||
{"user2", "pass2"},
|
||||
{"user3", "pass3"},
|
||||
},
|
||||
}).
|
||||
POST("/api/login").
|
||||
WithBody(map[string]interface{}{
|
||||
"username": "$username",
|
||||
"password": "$password",
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. 参数策略配置
|
||||
|
||||
```go
|
||||
hrp.NewStep("随机测试数据").
|
||||
WithParameters(map[string]interface{}{
|
||||
"data": []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||
}).
|
||||
WithParametersSetting(
|
||||
hrp.WithRandomOrder(), // 随机选择
|
||||
hrp.WithLimit(3), // 只执行3次
|
||||
).
|
||||
POST("/api/test").
|
||||
WithBody(map[string]interface{}{
|
||||
"value": "$data",
|
||||
})
|
||||
```
|
||||
|
||||
#### 4. 循环配置
|
||||
|
||||
```go
|
||||
// 推荐方式:使用 Loop 方法
|
||||
hrp.NewStep("循环测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京"},
|
||||
}).
|
||||
Loop(3). // 每个参数重复执行3次
|
||||
Android().
|
||||
StartToGoal("搜索「$query」")
|
||||
|
||||
// 总执行次数:2 × 3 = 6 次
|
||||
// 执行顺序:成都, 北京, 成都, 北京, 成都, 北京
|
||||
```
|
||||
|
||||
### 参数策略选项
|
||||
|
||||
- `sequential`: 顺序执行(默认)
|
||||
- `random`: 随机选择
|
||||
- `unique`: 唯一值,避免重复
|
||||
|
||||
### 高级特性
|
||||
|
||||
#### 1. 与现有功能兼容
|
||||
|
||||
步骤级参数化与现有功能完全兼容:
|
||||
|
||||
- ✅ 支持 Loop 循环
|
||||
- ✅ 支持变量提取和校验
|
||||
- ✅ 支持 SetupHooks 和 TeardownHooks
|
||||
- ✅ 支持 failfast 控制
|
||||
|
||||
#### 2. 步骤命名规则
|
||||
|
||||
- 无参数,无循环:`步骤名称`
|
||||
- 无参数,有循环:`步骤名称_loop_N`
|
||||
- 有参数,无循环:`步骤名称 [params_N]`
|
||||
- 有参数,有循环:`步骤名称 [loop_M_params_N]`(外层循环优化)
|
||||
|
||||
#### 3. 变量恢复机制
|
||||
|
||||
每次执行完成后,会自动恢复原始的步骤变量,避免副作用影响后续执行。
|
||||
|
||||
### 完整示例
|
||||
|
||||
```go
|
||||
func TestStepParametersExample(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("步骤级参数化示例"),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("多查询词搜索测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []interface{}{"成都", "北京", "重庆"},
|
||||
}).
|
||||
WithParametersSetting(
|
||||
hrp.WithSequentialOrder(),
|
||||
hrp.WithLimit(3),
|
||||
).
|
||||
Android().
|
||||
StartToGoal("搜索「$query」并等待结果加载").
|
||||
AIQuery("提取搜索结果"),
|
||||
hrp.NewStep("循环执行测试").
|
||||
WithParameters(map[string]interface{}{
|
||||
"data": []interface{}{1, 2, 3},
|
||||
}).
|
||||
Loop(2). // 每个参数执行2次
|
||||
GET("/api/test").
|
||||
WithParams(map[string]interface{}{
|
||||
"value": "$data",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
// 预期执行:
|
||||
// 第一个步骤:成都, 北京, 重庆 = 3次
|
||||
// 第二个步骤:1, 2, 3, 1, 2, 3 = 6次
|
||||
|
||||
err := hrp.NewRunner(t).Run(testCase)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
## 对比与选择
|
||||
|
||||
| 特性 | 测试用例层级 (Testcase-Level) | 测试步骤层级 (Step-Level) |
|
||||
| :--- | :--- | :--- |
|
||||
| **适用场景** | 完整的业务流程验证 | 单个功能点或API的验证 |
|
||||
| **数据作用域** | 整个 `TestCase` 中的所有 `Step` | 单个 `Step` |
|
||||
| **报告结果** | 每组参数生成一个独立的测试报告 | 所有参数在同一个测试报告中 |
|
||||
| **性能** | 开销较大,每次都需执行完整流程 | 开销较小,仅重复执行单个步骤 |
|
||||
| **配置方法** | `hrp.TestCase{ Parameters: ... }` | `hrp.Step{}.WithParameters(...)` |
|
||||
|
||||
**选择建议**:
|
||||
- 当您需要用多组数据测试一个完整的端到端流程时(例如,不同的用户配置),请使用**测试用例层级**参数化。
|
||||
- 当您只需要在一个流程中,针对某个特定步骤进行多次数据测试时(例如,测试接口对不同输入的响应),请使用**测试步骤层级**参数化。
|
||||
|
||||
## 变量优先级
|
||||
|
||||
HttpRunner 中的变量优先级遵循覆盖原则,顺序如下(从高到低):
|
||||
|
||||
1. **步骤级参数化变量** (`WithParameters`)
|
||||
2. **用例级参数化变量** (`TestCase.Parameters`)
|
||||
3. **步骤级变量** (`WithVariables`)
|
||||
4. **会话变量** (前面步骤提取的变量)
|
||||
5. **配置变量** (`Config.Variables`)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API 调用顺序**:步骤级参数化方法必须在步骤类型方法(如 `Android()`、`POST()` 等)之前调用
|
||||
2. **参数继承**:`StepMobile` 等步骤类型继承 `StepConfig`,参数会正确传递
|
||||
3. **编译时检查**:错误的调用顺序会在编译时报错,确保API使用正确性
|
||||
4. **变量覆盖**:Parameters 会覆盖同名的 Variables,但不会影响其他变量
|
||||
5. **变量恢复**:每次执行后自动恢复原始变量,避免副作用
|
||||
6. **执行顺序**:步骤级参数化采用外层循环优化,先遍历所有参数,再进行下一轮循环
|
||||
7. **Option API 推荐**:新项目建议使用 option 模式的 API,更加灵活和简洁
|
||||
135
examples/parameters/main_test.go
Normal file
135
examples/parameters/main_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package parameters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
hrp "github.com/httprunner/httprunner/v5"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestParametersExecutionScenarios 涵盖了参数化的核心执行场景,
|
||||
// 包括纯参数驱动、参数与循环结合,以及使用随机和限制等设置。
|
||||
func TestParametersExecutionScenarios(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("测试参数化核心执行场景").
|
||||
WithVariables(map[string]interface{}{"loops": 2}),
|
||||
TestSteps: []hrp.IStep{
|
||||
// 场景1: 纯参数驱动
|
||||
hrp.NewStep("API请求 - 纯参数").
|
||||
WithParameters(map[string]interface{}{
|
||||
"arg1": []int{10, 20},
|
||||
"arg2": []string{"a", "b"},
|
||||
}).
|
||||
GET("https://postman-echo.com/get").
|
||||
WithParams(map[string]interface{}{"p1": "$arg1", "p2": "$arg2"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
|
||||
// 场景2: 参数与 Loops 结合
|
||||
hrp.NewStep("API请求 - 参数与Loops结合").
|
||||
WithParameters(map[string]interface{}{
|
||||
"word": []string{"hello", "world"},
|
||||
}).
|
||||
Loop(3). // 每个参数执行3次
|
||||
GET("https://postman-echo.com/get").
|
||||
WithParams(map[string]interface{}{"search": "$word"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
|
||||
// 场景3: 参数设置 (随机, 限制)
|
||||
hrp.NewStep("API请求 - 参数设置").
|
||||
WithParameters(map[string]interface{}{
|
||||
"city": []string{"chengdu", "beijing", "shanghai", "guangzhou"},
|
||||
}).
|
||||
WithParametersSetting(
|
||||
hrp.WithRandomOrder(), // 随机顺序
|
||||
hrp.WithLimit(2), // 总共执行2次
|
||||
).
|
||||
GET("https://postman-echo.com/get").
|
||||
WithParams(map[string]interface{}{"city": "$city"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
},
|
||||
}
|
||||
err := hrp.NewRunner(t).Run(testCase)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
// TestParametersVariableOverride 用于验证参数(parameters)如何覆盖测试用例配置(config)
|
||||
// 和步骤(step)中定义的同名变量(variables)。
|
||||
func TestParametersVariableOverride(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("测试参数覆盖变量的优先级").
|
||||
WithVariables(map[string]interface{}{
|
||||
"p1": "config_level", // 将被步骤级变量覆盖
|
||||
"p2": "config_level", // 将被参数覆盖
|
||||
}),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("API请求 - 验证变量覆盖").
|
||||
WithVariables(map[string]interface{}{
|
||||
"p1": "step_level", // 不会被参数覆盖, 最终值为 "step_level"
|
||||
"p2": "step_level", // 会被参数覆盖
|
||||
"p3": "step_level", // 新增的步骤级变量
|
||||
}).
|
||||
WithParameters(map[string]interface{}{
|
||||
"p2-p4": [][]interface{}{
|
||||
{"param_level_2", "param_level_4_a"},
|
||||
{"param_level_2", "param_level_4_b"},
|
||||
},
|
||||
}).
|
||||
GET("https://postman-echo.com/get").
|
||||
WithParams(map[string]interface{}{
|
||||
"param1": "$p1", // 预期: step_level
|
||||
"param2": "$p2", // 预期: param_level_2
|
||||
"param3": "$p3", // 预期: step_level
|
||||
"param4": "$p4", // 预期: param_level_4_a/b
|
||||
}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("body.args.param1", "step_level", "p1 should be step_level").
|
||||
AssertEqual("body.args.param2", "param_level_2", "p2 should be param_level_2").
|
||||
AssertEqual("body.args.param3", "step_level", "p3 should be step_level"),
|
||||
},
|
||||
}
|
||||
err := hrp.NewRunner(t).Run(testCase)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
// TestParametersForMobileUI 演示了如何在移动端UI测试中使用参数化来驱动测试。
|
||||
func TestParametersForMobileUI(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("小红书UI参数化搜索").
|
||||
SetAIOptions(option.WithLLMConfig(
|
||||
option.NewLLMServiceConfig(option.DOUBAO_1_5_UI_TARS_250328),
|
||||
)),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动APP").
|
||||
Android().
|
||||
AppLaunch("com.xingin.xhs").
|
||||
Sleep(5).
|
||||
Validate().
|
||||
AssertAppInForeground("com.xingin.xhs"),
|
||||
hrp.NewStep("UI搜索 - 单参数").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query": []string{"成都", "北京"},
|
||||
}).
|
||||
Android().
|
||||
StartToGoal("进入搜索框,输入「$query」,等待搜索建议出现").
|
||||
Sleep(2),
|
||||
hrp.NewStep("UI搜索 - 复合参数").
|
||||
WithParameters(map[string]interface{}{
|
||||
"query-category": [][]string{
|
||||
{"美食", "食物"},
|
||||
{"旅游", "地点"},
|
||||
},
|
||||
}).
|
||||
Android().
|
||||
StartToGoal("进入搜索框,输入「$query」,并确认其类别为「$category」,等待搜索建议出现").
|
||||
Sleep(2),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testCase)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
v5.0.0-beta-2506231504
|
||||
v5.0.0-beta-2506232134
|
||||
|
||||
@@ -11,10 +11,51 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ParametersOption defines options for parameters configuration
|
||||
type ParametersOption func(*TParamsConfig)
|
||||
|
||||
// WithSequentialOrder sets parameters to be picked in sequential order
|
||||
func WithSequentialOrder() ParametersOption {
|
||||
return func(config *TParamsConfig) {
|
||||
config.PickOrder = iteratorPickOrder("sequential")
|
||||
}
|
||||
}
|
||||
|
||||
// WithRandomOrder sets parameters to be picked in random order
|
||||
func WithRandomOrder() ParametersOption {
|
||||
return func(config *TParamsConfig) {
|
||||
config.PickOrder = iteratorPickOrder("random")
|
||||
}
|
||||
}
|
||||
|
||||
// WithUniqueOrder sets parameters to be picked uniquely (no duplicates)
|
||||
func WithUniqueOrder() ParametersOption {
|
||||
return func(config *TParamsConfig) {
|
||||
config.PickOrder = iteratorPickOrder("unique")
|
||||
}
|
||||
}
|
||||
|
||||
// WithLimit sets the limit count for parameter iteration
|
||||
func WithLimit(limit int) ParametersOption {
|
||||
return func(config *TParamsConfig) {
|
||||
config.Limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
// WithStrategy sets individual strategy for a specific parameter
|
||||
func WithStrategy(paramName string, strategy IteratorStrategy) ParametersOption {
|
||||
return func(config *TParamsConfig) {
|
||||
if config.Strategies == nil {
|
||||
config.Strategies = make(map[string]IteratorStrategy)
|
||||
}
|
||||
config.Strategies[paramName] = strategy
|
||||
}
|
||||
}
|
||||
|
||||
type TParamsConfig struct {
|
||||
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` // overall pick-order strategy
|
||||
Strategies map[string]IteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters
|
||||
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
|
||||
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"` // limit count for parameter iteration
|
||||
}
|
||||
|
||||
type iteratorPickOrder string
|
||||
|
||||
195
runner.go
195
runner.go
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
@@ -734,6 +735,98 @@ const (
|
||||
RUN_STEP_END = "run step end"
|
||||
)
|
||||
|
||||
// executionTask holds the necessary information for a single step execution.
|
||||
type executionTask struct {
|
||||
stepName string
|
||||
parameters map[string]interface{}
|
||||
}
|
||||
|
||||
// formatParameters formats parameter values into a string for display in step names.
|
||||
// e.g. {"foo": "bar", "age": 18} -> "bar-18"
|
||||
func formatParameters(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// sort keys to ensure consistent order
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var values []string
|
||||
for _, k := range keys {
|
||||
values = append(values, fmt.Sprintf("%v", params[k]))
|
||||
}
|
||||
return strings.Join(values, "-")
|
||||
}
|
||||
|
||||
// generateExecutionTasks generates a list of execution tasks based on step parameters and loops.
|
||||
func (r *SessionRunner) generateExecutionTasks(step IStep) ([]executionTask, error) {
|
||||
stepConfig := step.Config()
|
||||
stepName := step.Name()
|
||||
|
||||
// determine effective loop times
|
||||
loopTimes := stepConfig.Loops
|
||||
if loopTimes <= 0 {
|
||||
loopTimes = 1 // default to 1 if not set
|
||||
}
|
||||
|
||||
// initialize parameters iterator
|
||||
parametersIterator, err := r.caseRunner.parser.InitParametersIterator(&TConfig{
|
||||
Parameters: stepConfig.Parameters,
|
||||
ParametersSetting: stepConfig.ParametersSetting,
|
||||
Variables: stepConfig.Variables,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize parameters iterator")
|
||||
}
|
||||
|
||||
// collect all parameter combinations first
|
||||
var allParameters []map[string]interface{}
|
||||
if parametersIterator != nil {
|
||||
for parametersIterator.HasNext() {
|
||||
allParameters = append(allParameters, parametersIterator.Next())
|
||||
}
|
||||
}
|
||||
|
||||
// if no parameters are specified, but loop times are set,
|
||||
// we should run the step loopTimes with empty parameters.
|
||||
if len(allParameters) == 0 && loopTimes > 0 {
|
||||
allParameters = append(allParameters, make(map[string]interface{}))
|
||||
}
|
||||
|
||||
// generate execution tasks
|
||||
var tasks []executionTask
|
||||
for loopIndex := 1; loopIndex <= loopTimes; loopIndex++ {
|
||||
for _, params := range allParameters {
|
||||
// determine step name based on parameters and loops
|
||||
currentStepName := stepName
|
||||
hasParameters := len(params) > 0
|
||||
hasLoops := loopTimes > 1
|
||||
|
||||
if hasParameters {
|
||||
paramStr := formatParameters(params)
|
||||
if hasLoops {
|
||||
currentStepName = fmt.Sprintf("%s [loop_%d_params_%s]", stepName, loopIndex, paramStr)
|
||||
} else {
|
||||
currentStepName = fmt.Sprintf("%s [params_%s]", stepName, paramStr)
|
||||
}
|
||||
} else if hasLoops {
|
||||
currentStepName = fmt.Sprintf("%s_loop_%d", stepName, loopIndex)
|
||||
}
|
||||
|
||||
tasks = append(tasks, executionTask{
|
||||
stepName: currentStepName,
|
||||
parameters: params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error) {
|
||||
// check for interrupt signal before running step
|
||||
select {
|
||||
@@ -753,56 +846,90 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
|
||||
|
||||
stepName := step.Name()
|
||||
stepType := string(step.Type())
|
||||
|
||||
log.Info().Str("step", stepName).Str("type", stepType).Msg(RUN_STEP_START)
|
||||
|
||||
// run times of step
|
||||
loopTimes := step.Config().Loops
|
||||
if loopTimes == 0 {
|
||||
loopTimes = 1 // default run once
|
||||
} else if loopTimes > 1 {
|
||||
log.Info().Int("loops", loopTimes).Msg("set multiple loop times")
|
||||
// execute step with parameters iterator
|
||||
tasks, err := r.generateExecutionTasks(step)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate execution tasks")
|
||||
}
|
||||
|
||||
// run step with specified loop times
|
||||
for i := 1; i <= loopTimes; i++ {
|
||||
var loopIndex string
|
||||
if loopTimes > 1 {
|
||||
log.Info().Int("index", i).Msg("start running step in loop")
|
||||
loopIndex = fmt.Sprintf("_loop_%d", i)
|
||||
var stepResults []*StepResult
|
||||
|
||||
// execute with loops as outer iteration
|
||||
for _, task := range tasks {
|
||||
// execute step with merged variables
|
||||
stepResult, err := r.executeStepWithVariables(step, task.stepName, task.parameters)
|
||||
if err != nil {
|
||||
if r.caseRunner.hrpRunner.failfast {
|
||||
return nil, errors.Wrap(err, "execute step failed")
|
||||
}
|
||||
log.Error().Err(err).Str("step", task.stepName).Msg("execute step failed")
|
||||
}
|
||||
|
||||
// run step
|
||||
stepResult, err = step.Run(r)
|
||||
stepResult.Name = stepName + loopIndex
|
||||
stepResults = append(stepResults, stepResult)
|
||||
}
|
||||
|
||||
// add step result to summary
|
||||
r.summary.AddStepResult(stepResult)
|
||||
|
||||
// update extracted variables
|
||||
for k, v := range stepResult.ExportVars {
|
||||
r.sessionVariables[k] = v
|
||||
// return the last step result, or nil if no steps were executed
|
||||
if len(stepResults) > 0 {
|
||||
// add all step results to summary
|
||||
for _, result := range stepResults {
|
||||
r.summary.AddStepResult(result)
|
||||
// update extracted variables from the last result
|
||||
for k, v := range result.ExportVars {
|
||||
r.sessionVariables[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// run step success
|
||||
if err == nil {
|
||||
// log final result
|
||||
lastResult := stepResults[len(stepResults)-1]
|
||||
if lastResult.Success {
|
||||
log.Info().Str("step", stepName).
|
||||
Str("type", stepType).
|
||||
Bool("success", true).
|
||||
Int64("elapsed(ms)", stepResult.Elapsed).
|
||||
Interface("exportVars", stepResult.ExportVars).
|
||||
Int64("elapsed(ms)", lastResult.Elapsed).
|
||||
Interface("exportVars", lastResult.ExportVars).
|
||||
Msg(RUN_STEP_END)
|
||||
} else {
|
||||
log.Error().Str("step", stepName).
|
||||
Str("type", stepType).
|
||||
Bool("success", false).
|
||||
Int64("elapsed(ms)", lastResult.Elapsed).
|
||||
Msg(RUN_STEP_END)
|
||||
continue
|
||||
}
|
||||
// run step failed
|
||||
log.Error().Err(err).Str("step", stepName).
|
||||
Str("type", stepType).
|
||||
Bool("success", false).
|
||||
Int64("elapsed(ms)", stepResult.Elapsed).
|
||||
Msg(RUN_STEP_END)
|
||||
return stepResult, err
|
||||
|
||||
return lastResult, nil
|
||||
}
|
||||
|
||||
return stepResult, nil
|
||||
return nil, errors.New("no steps were executed")
|
||||
}
|
||||
|
||||
// executeStepWithVariables executes a single step with given parameters
|
||||
// parameters will override step variables with the same name
|
||||
func (r *SessionRunner) executeStepWithVariables(step IStep, stepName string, parameters map[string]interface{}) (stepResult *StepResult, err error) {
|
||||
stepConfig := step.Config()
|
||||
|
||||
// backup original variables
|
||||
originalVariables := make(map[string]interface{})
|
||||
for k, v := range stepConfig.Variables {
|
||||
originalVariables[k] = v
|
||||
}
|
||||
|
||||
// merge parameters into step variables
|
||||
// parameters have higher priority than variables
|
||||
for k, v := range parameters {
|
||||
stepConfig.Variables[k] = v
|
||||
}
|
||||
|
||||
// execute step
|
||||
stepResult, err = step.Run(r)
|
||||
stepResult.Name = stepName
|
||||
|
||||
// restore original variables to avoid side effects
|
||||
stepConfig.Variables = originalVariables
|
||||
|
||||
return stepResult, err
|
||||
}
|
||||
|
||||
func (r *SessionRunner) GetSummary() *TestCaseSummary {
|
||||
|
||||
20
step.go
20
step.go
@@ -27,15 +27,17 @@ const (
|
||||
)
|
||||
|
||||
type StepConfig struct {
|
||||
StepName string `json:"name" yaml:"name"` // required
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
StepExport []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Loops int `json:"loops,omitempty" yaml:"loops,omitempty"`
|
||||
AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step
|
||||
StepName string `json:"name" yaml:"name"` // required
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
StepExport []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Loops int `json:"loops,omitempty" yaml:"loops,omitempty"`
|
||||
AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step
|
||||
}
|
||||
|
||||
// define struct for teststep
|
||||
|
||||
@@ -543,6 +543,26 @@ func (s *StepRequest) WithVariables(variables map[string]interface{}) *StepReque
|
||||
return s
|
||||
}
|
||||
|
||||
// WithParameters sets parameters for step-level data driven
|
||||
func (s *StepRequest) WithParameters(parameters map[string]interface{}) *StepRequest {
|
||||
s.Parameters = parameters
|
||||
return s
|
||||
}
|
||||
|
||||
// WithParametersSetting sets parameters setting for step-level data driven
|
||||
func (s *StepRequest) WithParametersSetting(options ...ParametersOption) *StepRequest {
|
||||
if s.ParametersSetting == nil {
|
||||
s.ParametersSetting = &TParamsConfig{}
|
||||
}
|
||||
|
||||
// apply all options
|
||||
for _, option := range options {
|
||||
option(s.ParametersSetting)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// SetupHook adds a setup hook for current teststep.
|
||||
func (s *StepRequest) SetupHook(hook string) *StepRequest {
|
||||
s.SetupHooks = append(s.SetupHooks, hook)
|
||||
@@ -557,7 +577,7 @@ func (s *StepRequest) HTTP2() *StepRequest {
|
||||
return s
|
||||
}
|
||||
|
||||
// Loop specify running times for the current step
|
||||
// Loop sets loop count for step execution.
|
||||
func (s *StepRequest) Loop(times int) *StepRequest {
|
||||
s.Loops = times
|
||||
return s
|
||||
|
||||
Reference in New Issue
Block a user