mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 23:51:25 +08:00
feat: data-driven.
This commit is contained in:
@@ -46,15 +46,12 @@ func (b *hrpBoomer) Run(testcases ...ITestCase) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cfg := testcase.Config.ToStruct()
|
cfg := testcase.Config.ToStruct()
|
||||||
parameters := getParameters(testcase.Config)
|
err = initParameterIterator(cfg, "boomer")
|
||||||
if parameters == nil {
|
if err != nil {
|
||||||
parameters = []map[string]interface{}{{}}
|
panic(err)
|
||||||
}
|
|
||||||
for _, parameter := range parameters {
|
|
||||||
cfg.Variables = mergeVariables(parameter, cfg.Variables)
|
|
||||||
task := b.convertBoomerTask(testcase)
|
|
||||||
taskSlice = append(taskSlice, task)
|
|
||||||
}
|
}
|
||||||
|
task := b.convertBoomerTask(testcase)
|
||||||
|
taskSlice = append(taskSlice, task)
|
||||||
}
|
}
|
||||||
b.Boomer.Run(taskSlice...)
|
b.Boomer.Run(taskSlice...)
|
||||||
}
|
}
|
||||||
@@ -71,6 +68,10 @@ func (b *hrpBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
|
|||||||
testcaseSuccess := true // flag whole testcase result
|
testcaseSuccess := true // flag whole testcase result
|
||||||
var transactionSuccess = true // flag current transaction result
|
var transactionSuccess = true // flag current transaction result
|
||||||
|
|
||||||
|
cfg := testcase.Config.ToStruct()
|
||||||
|
if it := cfg.ParameterIterator; it.HasNext() {
|
||||||
|
cfg.Variables = mergeVariables(it.Next(), cfg.Variables)
|
||||||
|
}
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
for index, step := range testcase.TestSteps {
|
for index, step := range testcase.TestSteps {
|
||||||
stepData, err := runner.runStep(index)
|
stepData, err := runner.runStep(index)
|
||||||
|
|||||||
@@ -6,14 +6,11 @@
|
|||||||
"iOS/10.1",
|
"iOS/10.1",
|
||||||
"iOS/10.2"
|
"iOS/10.2"
|
||||||
],
|
],
|
||||||
"username1-password1": [
|
|
||||||
["a", "123"],
|
|
||||||
["b", "456"]
|
|
||||||
],
|
|
||||||
"username-password": "${parameterize(examples/account.csv)}"
|
"username-password": "${parameterize(examples/account.csv)}"
|
||||||
},
|
},
|
||||||
"parameters_setting": {
|
"parameters_setting": {
|
||||||
"strategy": "random"
|
"strategy": "random",
|
||||||
|
"iteration": 1
|
||||||
},
|
},
|
||||||
"variables": {
|
"variables": {
|
||||||
"app_version": "f1"
|
"app_version": "f1"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ config:
|
|||||||
username-password: ${parameterize(examples/account.csv)}
|
username-password: ${parameterize(examples/account.csv)}
|
||||||
parameters_setting:
|
parameters_setting:
|
||||||
strategy: random
|
strategy: random
|
||||||
|
iteration: 1
|
||||||
variables:
|
variables:
|
||||||
app_version: f1
|
app_version: f1
|
||||||
base_url: "https://postman-echo.com"
|
base_url: "https://postman-echo.com"
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package hrp
|
package hrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
httpGET string = "GET"
|
httpGET string = "GET"
|
||||||
httpHEAD string = "HEAD"
|
httpHEAD string = "HEAD"
|
||||||
@@ -18,11 +24,56 @@ type TConfig struct {
|
|||||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
||||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||||
ParametersSetting map[string]interface{} `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||||
|
ParameterIterator *Iterator `json:"parameterIterator,omitempty" yaml:"parameterIterator,omitempty"`
|
||||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TParamsConfig struct {
|
||||||
|
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
|
||||||
|
Iteration int `json:"iteration,omitempty" yaml:"iteration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramsType []map[string]interface{}
|
||||||
|
|
||||||
|
type Iterator struct {
|
||||||
|
data paramsType
|
||||||
|
strategy string
|
||||||
|
iteration int
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params paramsType) Iterator() *Iterator {
|
||||||
|
return &Iterator{
|
||||||
|
data: params,
|
||||||
|
iteration: len(params),
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iter *Iterator) HasNext() bool {
|
||||||
|
if iter.iteration == -1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return iter.index < iter.iteration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iter *Iterator) Next() (value map[string]interface{}) {
|
||||||
|
iter.index++
|
||||||
|
if len(iter.data) == 0 {
|
||||||
|
return map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if strings.ToLower(iter.strategy) == "random" {
|
||||||
|
randSource := rand.New(rand.NewSource(time.Now().Unix()))
|
||||||
|
randIndex := randSource.Intn(len(iter.data))
|
||||||
|
value = iter.data[randIndex]
|
||||||
|
} else {
|
||||||
|
value = iter.data[iter.index%len(iter.data)]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// Request represents HTTP request data structure.
|
// Request represents HTTP request data structure.
|
||||||
// This is used for teststep.
|
// This is used for teststep.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
|
|||||||
@@ -3,16 +3,13 @@ package hrp
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"github.com/maja42/goval"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/maja42/goval"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
"github.com/httprunner/hrp/internal/builtin"
|
"github.com/httprunner/hrp/internal/builtin"
|
||||||
)
|
)
|
||||||
@@ -496,19 +493,6 @@ func findallVariables(raw string) variableSet {
|
|||||||
return varSet
|
return varSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func shuffleCartesianProduct(slice []map[string]interface{}) {
|
|
||||||
if len(slice) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r := rand.New(rand.NewSource(time.Now().Unix()))
|
|
||||||
for len(slice) > 0 {
|
|
||||||
n := len(slice)
|
|
||||||
randIndex := r.Intn(n)
|
|
||||||
slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1]
|
|
||||||
slice = slice[:n-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func genCartesianProduct(params [][]map[string]interface{}) []map[string]interface{} {
|
func genCartesianProduct(params [][]map[string]interface{}) []map[string]interface{} {
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -527,19 +511,6 @@ func genCartesianProduct(params [][]map[string]interface{}) []map[string]interfa
|
|||||||
return cartesianProduct
|
return cartesianProduct
|
||||||
}
|
}
|
||||||
|
|
||||||
func getParameters(config IConfig) []map[string]interface{} {
|
|
||||||
cfg := config.ToStruct()
|
|
||||||
// parse config parameters
|
|
||||||
parsedParams, err := parseParameters(cfg.Parameters, cfg.Variables)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed")
|
|
||||||
}
|
|
||||||
if cfg.ParametersSetting["strategy"] != nil && strings.ToLower(cfg.ParametersSetting["strategy"].(string)) == "random" {
|
|
||||||
shuffleCartesianProduct(parsedParams)
|
|
||||||
}
|
|
||||||
return parsedParams
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) ([]map[string]interface{}, error) {
|
func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) ([]map[string]interface{}, error) {
|
||||||
if len(parameters) == 0 {
|
if len(parameters) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -562,9 +533,9 @@ func parseParameters(parameters map[string]interface{}, variablesMapping map[str
|
|||||||
log.Error().Interface("parameterContent", parsedParameterRawValue).Msg("[parseParameters] parsed parameter content should be Slice, got %v")
|
log.Error().Interface("parameterContent", parsedParameterRawValue).Msg("[parseParameters] parsed parameter content should be Slice, got %v")
|
||||||
return nil, errors.New("parsed parameter content should be Slice")
|
return nil, errors.New("parsed parameter content should be Slice")
|
||||||
}
|
}
|
||||||
parameterSlice, err = handleSlice(k, parsedParameterRawValue.Interface())
|
parameterSlice, err = parseSlice(k, parsedParameterRawValue.Interface())
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
parameterSlice, err = handleSlice(k, rawValue.Interface())
|
parameterSlice, err = parseSlice(k, rawValue.Interface())
|
||||||
default:
|
default:
|
||||||
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be Slice or Text(variables or functions call)")
|
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be Slice or Text(variables or functions call)")
|
||||||
return nil, errors.New("parameter content should be Slice or Text(variables or functions call)")
|
return nil, errors.New("parameter content should be Slice or Text(variables or functions call)")
|
||||||
@@ -577,10 +548,13 @@ func parseParameters(parameters map[string]interface{}, variablesMapping map[str
|
|||||||
return genCartesianProduct(parsedParametersSlice), nil
|
return genCartesianProduct(parsedParametersSlice), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSlice(parameterName string, parameterContent interface{}) ([]map[string]interface{}, error) {
|
func parseSlice(parameterName string, parameterContent interface{}) ([]map[string]interface{}, error) {
|
||||||
parameterNameSlice := strings.Split(parameterName, "-")
|
parameterNameSlice := strings.Split(parameterName, "-")
|
||||||
var parameterSlice []map[string]interface{}
|
var parameterSlice []map[string]interface{}
|
||||||
parameterContentSlice := reflect.ValueOf(parameterContent)
|
parameterContentSlice := reflect.ValueOf(parameterContent)
|
||||||
|
if parameterContentSlice.Kind() != reflect.Slice {
|
||||||
|
return nil, errors.New("parameterContent should be Slice")
|
||||||
|
}
|
||||||
for i := 0; i < parameterContentSlice.Len(); i++ {
|
for i := 0; i < parameterContentSlice.Len(); i++ {
|
||||||
parameterMap := make(map[string]interface{})
|
parameterMap := make(map[string]interface{})
|
||||||
elem := reflect.ValueOf(parameterContentSlice.Index(i).Interface())
|
elem := reflect.ValueOf(parameterContentSlice.Index(i).Interface())
|
||||||
@@ -620,3 +594,31 @@ func handleSlice(parameterName string, parameterContent interface{}) ([]map[stri
|
|||||||
}
|
}
|
||||||
return parameterSlice, nil
|
return parameterSlice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initParameterIterator(cfg *TConfig, mode string) (err error) {
|
||||||
|
var parameters paramsType
|
||||||
|
parameters, err = parseParameters(cfg.Parameters, cfg.Variables)
|
||||||
|
cfg.ParameterIterator = parameters.Iterator()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// parse config parameters setting
|
||||||
|
if cfg.ParametersSetting == nil {
|
||||||
|
cfg.ParametersSetting = &TParamsConfig{}
|
||||||
|
}
|
||||||
|
if len(cfg.ParametersSetting.Strategy) == 0 {
|
||||||
|
cfg.ParametersSetting.Strategy = "sequential"
|
||||||
|
} else {
|
||||||
|
cfg.ParametersSetting.Strategy = strings.ToLower(cfg.ParametersSetting.Strategy)
|
||||||
|
}
|
||||||
|
cfg.ParameterIterator.strategy = cfg.ParametersSetting.Strategy
|
||||||
|
if mode == "boomer" {
|
||||||
|
cfg.ParametersSetting.Iteration = -1
|
||||||
|
cfg.ParameterIterator.iteration = cfg.ParametersSetting.Iteration
|
||||||
|
} else {
|
||||||
|
if cfg.ParametersSetting.Iteration != 0 {
|
||||||
|
cfg.ParameterIterator.iteration = cfg.ParametersSetting.Iteration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+24
-2
@@ -692,7 +692,7 @@ func TestParseParametersError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleSlice(t *testing.T) {
|
func TestParseSlice(t *testing.T) {
|
||||||
testData := []struct {
|
testData := []struct {
|
||||||
rawVar1 string
|
rawVar1 string
|
||||||
rawVar2 interface{}
|
rawVar2 interface{}
|
||||||
@@ -724,9 +724,31 @@ func TestHandleSlice(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, data := range testData {
|
for _, data := range testData {
|
||||||
value, _ := handleSlice(data.rawVar1, data.rawVar2)
|
value, _ := parseSlice(data.rawVar1, data.rawVar2)
|
||||||
if !assert.Equal(t, data.expect, value) {
|
if !assert.Equal(t, data.expect, value) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSliceError(t *testing.T) {
|
||||||
|
testData := []struct {
|
||||||
|
rawVar1 string
|
||||||
|
rawVar2 interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"app_version",
|
||||||
|
123,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"app_version",
|
||||||
|
"123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, data := range testData {
|
||||||
|
_, err := parseSlice(data.rawVar1, data.rawVar2)
|
||||||
|
if !assert.Error(t, err) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,13 +141,17 @@ func (r *caseRunner) run() error {
|
|||||||
if err := r.parseConfig(config); err != nil {
|
if err := r.parseConfig(config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
cfg := config.ToStruct()
|
||||||
log.Info().Str("testcase", config.Name()).Msg("run testcase start")
|
log.Info().Str("testcase", config.Name()).Msg("run testcase start")
|
||||||
r.startTime = time.Now()
|
for it := cfg.ParameterIterator; it.HasNext(); {
|
||||||
for index := range r.TestCase.TestSteps {
|
cfg.Variables = mergeVariables(it.Next(), cfg.Variables)
|
||||||
_, err := r.runStep(index)
|
r.startTime = time.Now()
|
||||||
if err != nil {
|
for index := range r.TestCase.TestSteps {
|
||||||
if r.hrpRunner.failfast {
|
_, err := r.runStep(index)
|
||||||
return errors.Wrap(err, "abort running due to failfast setting")
|
if err != nil {
|
||||||
|
if r.hrpRunner.failfast {
|
||||||
|
return errors.Wrap(err, "abort running due to failfast setting")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,6 +486,12 @@ func (r *caseRunner) parseConfig(config IConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cfg.Variables = parsedVariables
|
cfg.Variables = parsedVariables
|
||||||
|
// parse config parameters
|
||||||
|
err = initParameterIterator(cfg, "runner")
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
// parse config name
|
// parse config name
|
||||||
parsedName, err := parseString(cfg.Name, cfg.Variables)
|
parsedName, err := parseString(cfg.Name, cfg.Variables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user