feat: add parameterization support for test cases and steps with configuration options

This commit is contained in:
lilong.129
2025-06-23 21:14:29 +08:00
parent 93007d5eb7
commit b320bbda31
8 changed files with 772 additions and 46 deletions

View File

@@ -88,6 +88,20 @@ func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
return c 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. // SetThinkTime sets think time config for current testcase.
func (c *TConfig) SetThinkTime(strategy ThinkTimeStrategy, cfg interface{}, limit float64) *TConfig { func (c *TConfig) SetThinkTime(strategy ThinkTimeStrategy, cfg interface{}, limit float64) *TConfig {
c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit} c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit}

387
docs/parameters.md Normal file
View 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更加灵活和简洁

View 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)
}

View File

@@ -1 +1 @@
v5.0.0-beta-2506231504 v5.0.0-beta-2506232134

View File

@@ -11,10 +11,51 @@ import (
"github.com/rs/zerolog/log" "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 { type TParamsConfig struct {
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` // overall pick-order strategy 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 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 type iteratorPickOrder string

195
runner.go
View File

@@ -11,6 +11,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"reflect" "reflect"
"sort"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
@@ -734,6 +735,98 @@ const (
RUN_STEP_END = "run step end" 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) { func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error) {
// check for interrupt signal before running step // check for interrupt signal before running step
select { select {
@@ -753,56 +846,90 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
stepName := step.Name() stepName := step.Name()
stepType := string(step.Type()) stepType := string(step.Type())
log.Info().Str("step", stepName).Str("type", stepType).Msg(RUN_STEP_START) log.Info().Str("step", stepName).Str("type", stepType).Msg(RUN_STEP_START)
// run times of step // execute step with parameters iterator
loopTimes := step.Config().Loops tasks, err := r.generateExecutionTasks(step)
if loopTimes == 0 { if err != nil {
loopTimes = 1 // default run once return nil, errors.Wrap(err, "failed to generate execution tasks")
} else if loopTimes > 1 {
log.Info().Int("loops", loopTimes).Msg("set multiple loop times")
} }
// run step with specified loop times var stepResults []*StepResult
for i := 1; i <= loopTimes; i++ {
var loopIndex string // execute with loops as outer iteration
if loopTimes > 1 { for _, task := range tasks {
log.Info().Int("index", i).Msg("start running step in loop") // execute step with merged variables
loopIndex = fmt.Sprintf("_loop_%d", i) 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 stepResults = append(stepResults, stepResult)
stepResult, err = step.Run(r) }
stepResult.Name = stepName + loopIndex
// add step result to summary // return the last step result, or nil if no steps were executed
r.summary.AddStepResult(stepResult) if len(stepResults) > 0 {
// add all step results to summary
// update extracted variables for _, result := range stepResults {
for k, v := range stepResult.ExportVars { r.summary.AddStepResult(result)
r.sessionVariables[k] = v // update extracted variables from the last result
for k, v := range result.ExportVars {
r.sessionVariables[k] = v
}
} }
// run step success // log final result
if err == nil { lastResult := stepResults[len(stepResults)-1]
if lastResult.Success {
log.Info().Str("step", stepName). log.Info().Str("step", stepName).
Str("type", stepType). Str("type", stepType).
Bool("success", true). Bool("success", true).
Int64("elapsed(ms)", stepResult.Elapsed). Int64("elapsed(ms)", lastResult.Elapsed).
Interface("exportVars", stepResult.ExportVars). 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) Msg(RUN_STEP_END)
continue
} }
// run step failed
log.Error().Err(err).Str("step", stepName). return lastResult, nil
Str("type", stepType).
Bool("success", false).
Int64("elapsed(ms)", stepResult.Elapsed).
Msg(RUN_STEP_END)
return stepResult, err
} }
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 { func (r *SessionRunner) GetSummary() *TestCaseSummary {

20
step.go
View File

@@ -27,15 +27,17 @@ const (
) )
type StepConfig struct { type StepConfig struct {
StepName string `json:"name" yaml:"name"` // required StepName string `json:"name" yaml:"name"` // required
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
StepExport []string `json:"export,omitempty" yaml:"export,omitempty"` Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
Loops int `json:"loops,omitempty" yaml:"loops,omitempty"` Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
AutoPopupHandler bool `json:"auto_popup_handler,omitempty" yaml:"auto_popup_handler,omitempty"` // enable auto popup handler for this step 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 // define struct for teststep

View File

@@ -543,6 +543,26 @@ func (s *StepRequest) WithVariables(variables map[string]interface{}) *StepReque
return s 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. // SetupHook adds a setup hook for current teststep.
func (s *StepRequest) SetupHook(hook string) *StepRequest { func (s *StepRequest) SetupHook(hook string) *StepRequest {
s.SetupHooks = append(s.SetupHooks, hook) s.SetupHooks = append(s.SetupHooks, hook)
@@ -557,7 +577,7 @@ func (s *StepRequest) HTTP2() *StepRequest {
return s return s
} }
// Loop specify running times for the current step // Loop sets loop count for step execution.
func (s *StepRequest) Loop(times int) *StepRequest { func (s *StepRequest) Loop(times int) *StepRequest {
s.Loops = times s.Loops = times
return s return s