feat: data-driven.

This commit is contained in:
徐聪
2021-12-28 18:16:02 +08:00
parent 2f5afb5ffc
commit 43deb78b26
11 changed files with 268 additions and 90 deletions
+3
View File
@@ -47,6 +47,9 @@ func (b *hrpBoomer) Run(testcases ...ITestCase) {
} }
cfg := testcase.Config.ToStruct() cfg := testcase.Config.ToStruct()
parameters := getParameters(testcase.Config) parameters := getParameters(testcase.Config)
if parameters == nil {
parameters = []map[string]interface{}{{}}
}
for _, parameter := range parameters { for _, parameter := range parameters {
cfg.Variables = mergeVariables(parameter, cfg.Variables) cfg.Variables = mergeVariables(parameter, cfg.Variables)
task := b.convertBoomerTask(testcase) task := b.convertBoomerTask(testcase)
+3 -3
View File
@@ -100,20 +100,20 @@ func loadFromCSV(path string) []map[string]string {
path, err := filepath.Abs(path) path, err := filepath.Abs(path)
if err != nil { if err != nil {
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed") log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
return nil panic(err)
} }
log.Info().Str("path", path).Msg("load csv file") log.Info().Str("path", path).Msg("load csv file")
file, err := ioutil.ReadFile(path) file, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("load csv file failed") log.Error().Err(err).Msg("load csv file failed")
return nil panic(err)
} }
r := csv.NewReader(strings.NewReader(string(file))) r := csv.NewReader(strings.NewReader(string(file)))
content, err := r.ReadAll() content, err := r.ReadAll()
if err != nil { if err != nil {
log.Error().Err(err).Msg("parse csv file failed") log.Error().Err(err).Msg("parse csv file failed")
return nil panic(err)
} }
var result []map[string]string var result []map[string]string
for i := 1; i < len(content); i++ { for i := 1; i < len(content); i++ {
+4
View File
@@ -0,0 +1,4 @@
username,password
test1,111111
test2,222222
test3,333333
1 username password
2 test1 111111
3 test2 222222
4 test3 333333
-7
View File
@@ -7,11 +7,6 @@ config:
"n": 5 "n": 5
varFoo1: ${gen_random_string($n)} varFoo1: ${gen_random_string($n)}
varFoo2: ${max($a, $b)} varFoo2: ${max($a, $b)}
parameters:
username-password: ${parameterize(examples/account.csv)}
user_agent: ["iOS/10.1", "iOS/10.2"]
parameters_setting:
strategy: random
teststeps: teststeps:
- name: transaction 1 start - name: transaction 1 start
transaction: transaction:
@@ -24,8 +19,6 @@ teststeps:
params: params:
foo1: $varFoo1 foo1: $varFoo1
foo2: $varFoo2 foo2: $varFoo2
foo3: $username
foo4: $password
headers: headers:
User-Agent: HttpRunnerPlus User-Agent: HttpRunnerPlus
variables: variables:
+57
View File
@@ -0,0 +1,57 @@
{
"config": {
"name": "request methods testcase: validate with parameters",
"parameters": {
"user_agent": [
"iOS/10.1",
"iOS/10.2"
],
"username-password": "${parameterize(examples/account.csv)}",
"app_version": "${getAppVersion()}"
},
"parameters_setting": {
"strategy": "random"
},
"variables": {
"app_version": "f1"
},
"base_url": "https://postman-echo.com",
"verify": false
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "$username",
"foo2": "$password",
"foo3": "$app_version"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"foo3": "$foo3"
},
"headers": {
"User-Agent": "$user_agent,$app_version"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
},
{
"check": "body.args.foo3",
"assert": "not_equal",
"expect": "f1",
"msg": "check app version"
}
]
}
]
}
+38
View File
@@ -0,0 +1,38 @@
config:
name: "request methods testcase: validate with parameters"
parameters:
user_agent: ["iOS/10.1", "iOS/10.2"]
username-password: ${parameterize(examples/account.csv)}
app_version: ${getAppVersion()}
parameters_setting:
strategy: random
variables:
app_version: f1
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: get with params
variables:
foo1: $username
foo2: $password
foo3: $app_version
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
foo3: $foo3
headers:
User-Agent: $user_agent,$app_version
validate:
- check: status_code
assert: equals
expect: 200
msg: check status code
- check: body.args.foo3
assert: not_equal
expect: f1
msg: check app version
+5
View File
@@ -14,6 +14,7 @@ var Functions = map[string]interface{}{
"gen_random_string": genRandomString, // call with one argument "gen_random_string": genRandomString, // call with one argument
"max": math.Max, // call with two arguments "max": math.Max, // call with two arguments
"md5": MD5, "md5": MD5,
"getAppVersion": getAppVersion, // test
} }
func init() { func init() {
@@ -44,3 +45,7 @@ func MD5(str string) string {
hasher.Write([]byte(str)) hasher.Write([]byte(str))
return hex.EncodeToString(hasher.Sum(nil)) return hex.EncodeToString(hasher.Sum(nil))
} }
func getAppVersion() []float64 {
return []float64{3.1, 3.3}
}
+104 -41
View File
@@ -3,13 +3,16 @@ package hrp
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/maja42/goval" "math/rand"
"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"
) )
@@ -247,6 +250,15 @@ func mergeVariables(variables, overriddenVariables map[string]interface{}) map[s
return mergedVariables return mergedVariables
} }
func contains(s []string, e string) bool {
for _, a := range s {
if strings.EqualFold(a, e) {
return true
}
}
return false
}
func getMappingFunction(funcName string) (interface{}, error) { func getMappingFunction(funcName string) (interface{}, error) {
if function, ok := builtin.Functions[funcName]; ok { if function, ok := builtin.Functions[funcName]; ok {
// function is builtin // function is builtin
@@ -505,12 +517,43 @@ func findallVariables(raw string) variableSet {
return varSet return varSet
} }
func shuffleCartesianProduct(slice []map[string]interface{}) {
if slice == nil || 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{} {
if params == nil || len(params) == 0 {
return nil
}
var cartesianProduct []map[string]interface{}
cartesianProduct = params[0]
for i := 0; i < len(params)-1; i++ {
var tempProduct []map[string]interface{}
for _, param1 := range cartesianProduct {
for _, param2 := range params[i+1] {
tempProduct = append(tempProduct, mergeVariables(param1, param2))
}
}
cartesianProduct = tempProduct
}
return cartesianProduct
}
func getParameters(config IConfig) []map[string]interface{} { func getParameters(config IConfig) []map[string]interface{} {
cfg := config.ToStruct() cfg := config.ToStruct()
// parse config parameters // parse config parameters
parsedParams, err := parseParameters(cfg.Parameters, cfg.Variables) parsedParams, err := parseParameters(cfg.Parameters, cfg.Variables)
if err != nil { if err != nil {
log.Error().Interface("params", cfg.Parameters).Err(err).Msg("parse config parameters failed") 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" { if cfg.ParametersSetting["strategy"] != nil && strings.ToLower(cfg.ParametersSetting["strategy"].(string)) == "random" {
shuffleCartesianProduct(parsedParams) shuffleCartesianProduct(parsedParams)
@@ -519,10 +562,13 @@ func getParameters(config IConfig) []map[string]interface{} {
} }
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) {
var parsedParametersList [][]map[string]interface{} if parameters == nil || len(parameters) == 0 {
return nil, nil
}
var parsedParametersSlice [][]map[string]interface{}
for k, v := range parameters { for k, v := range parameters {
parameterNameList := strings.Split(k, "-") parameterNameSlice := strings.Split(k, "-")
var parameterList []map[string]interface{} var parameterSlice []map[string]interface{}
rawValue := reflect.ValueOf(v) rawValue := reflect.ValueOf(v)
switch rawValue.Kind() { switch rawValue.Kind() {
case reflect.String: case reflect.String:
@@ -533,55 +579,72 @@ func parseParameters(parameters map[string]interface{}, variablesMapping map[str
} }
parsedParameterRawValue := reflect.ValueOf(parsedParameterContent) parsedParameterRawValue := reflect.ValueOf(parsedParameterContent)
if parsedParameterRawValue.Kind() != reflect.Slice { if parsedParameterRawValue.Kind() != reflect.Slice {
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") log.Error().Interface("parameter", parameters).Msg("[parseParameters] parsed parameter content should be Slice, got %v")
return nil, errors.New("parameter content should be List or Text(variables or functions call)") return nil, errors.New("parsed parameter content should be Slice")
} }
for i := 0; i < parsedParameterRawValue.Len(); i++ { for i := 0; i < parsedParameterRawValue.Len(); i++ {
parameterMap := make(map[string]interface{}) parameterMap := make(map[string]interface{})
if parsedParameterRawValue.Index(i).Kind() == reflect.Map { // e.g.
for _, key := range parameterNameList { elem := reflect.ValueOf(parsedParameterRawValue.Index(i).Interface())
parameterMap[key] = parsedParameterRawValue.Index(i).MapIndex(reflect.ValueOf(key)).Interface() if elem.Kind() == reflect.Map {
} // e.g. [{"username": "test1", "password": "passwd1", "other": "111"}, {"username": "test2", "password": "passwd2", "other": ""222}]
} else if parsedParameterRawValue.Index(i).Kind() == reflect.Slice { // -> [{"username": "test1", "password": "passwd1"}, {"username": "test2", "password": "passwd2"}] (username, password in parameterNameSlice)
if len(parameterNameList) != parsedParameterRawValue.Index(i).Len() { for _, key := range parameterNameSlice {
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter name list and parameter content list should have the same length") if _, ok := elem.Interface().(map[string]string)[key]; ok {
return nil, errors.New("parameter name list and parameter content list should have the same length") parameterMap[key] = elem.MapIndex(reflect.ValueOf(key)).Interface()
}
for i := 0; i < parsedParameterRawValue.Index(i).Len(); i++ {
parameterMap[parameterNameList[i]] = parsedParameterRawValue.Index(i).Index(i).Interface()
}
} else if len(parameterNameList) == 1 {
parameterMap[parameterNameList[0]] = parsedParameterRawValue.Index(i).Interface()
} else { } else {
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name not found")
return nil, errors.New("parameter content should be List or Text(variables or functions call)") return nil, errors.New("parameter name not found")
} }
parameterList = append(parameterList, parameterMap) }
} else if elem.Kind() == reflect.Slice {
// e.g. [["test1", "passwd1"], ["test2", "passwd2"]] -> [{"username": "test1", "password": "passwd1"}, {"username": "test2", "password": "passwd2"}]
if len(parameterNameSlice) != elem.Len() {
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter name Slice and parameter content Slice should have the same length")
return nil, errors.New("parameter name Slice and parameter cjntent Slice should have the same length")
} else {
for j := 0; j < elem.Len(); j++ {
parameterMap[parameterNameSlice[j]] = elem.Index(j).Interface()
}
}
} else {
// e.g. ${getAppVersion()} -> [3.1, 3.0] -> [{"app_version": 3.1}, {"app_version": 3.0}]
if len(parameterNameSlice) != 1 {
log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name slice should have only one element when parameter content is string")
return nil, errors.New("parameter name slice should have only one element when parameter content is string")
}
parameterMap[parameterNameSlice[0]] = elem.Interface()
}
parameterSlice = append(parameterSlice, parameterMap)
} }
case reflect.Slice: case reflect.Slice:
for i := 0; i < rawValue.Len(); i++ { for i := 0; i < rawValue.Len(); i++ {
parameterMap := make(map[string]interface{}) parameterMap := make(map[string]interface{})
if rawValue.Index(i).Kind() == reflect.Interface || rawValue.Index(i).Kind() == reflect.String { elem := reflect.ValueOf(rawValue.Index(i).Interface())
parameterMap[parameterNameList[0]] = rawValue.Index(i).Interface() if elem.Kind() == reflect.Slice {
} else if rawValue.Index(i).Kind() == reflect.Slice { // e.g. username-password: [["test1", "passwd1"], ["test2", "passwd2"]]
if len(parameterNameList) != rawValue.Index(i).Len() { if len(parameterNameSlice) != elem.Len() {
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter name list and parameter content list should have the same length") log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter name Slice and parameter content Slice should have the same length")
return nil, errors.New("parameter name list and parameter content list should have the same length") return nil, errors.New("parameter name Slice and parameter content Slice should have the same length")
} }
for i := 0; i < rawValue.Index(i).Len(); i++ { for j := 0; j < elem.Len(); j++ {
parameterMap[parameterNameList[i]] = rawValue.Index(i).Index(i).Interface() parameterMap[parameterNameSlice[j]] = elem.Index(j).Interface()
} }
} else { } else {
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") // e.g. user_agent: ["iOS/10.1", "iOS/10.2"]
return nil, errors.New("parameter content should be List or Text(variables or functions call)") if len(parameterNameSlice) != 1 {
log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name slice should have only one element when parameter content is string")
return nil, errors.New("parameter name slice should have only one element when parameter content is string")
} }
parameterList = append(parameterList, parameterMap) parameterMap[parameterNameSlice[0]] = elem.Interface()
}
parameterSlice = append(parameterSlice, parameterMap)
} }
default: default:
log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") 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 List or Text(variables or functions call)") return nil, errors.New("parameter content should be Slice or Text(variables or functions call)")
} }
parsedParametersList = append(parsedParametersList, parameterList) parsedParametersSlice = append(parsedParametersSlice, parameterSlice)
} }
return genCartesianProduct(parsedParametersList), nil return genCartesianProduct(parsedParametersSlice), nil
} }
+53
View File
@@ -618,3 +618,56 @@ func TestFindallVariables(t *testing.T) {
} }
} }
} }
func TestParseParameters(t *testing.T) {
testData := []struct {
rawVars map[string]interface{}
expectVars []map[string]interface{}
}{
{
map[string]interface{}{"username-password": "${parameterize(examples/account.csv)}", "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
[]map[string]interface{}{{"username": "test1", "password": "111111", "user_agent": "IOS/10.1"},
{"username": "test1", "password": "111111", "user_agent": "IOS/10.2"},
{"username": "test2", "password": "222222", "user_agent": "IOS/10.1"},
{"username": "test2", "password": "222222", "user_agent": "IOS/10.2"},
{"username": "test3", "password": "333333", "user_agent": "IOS/10.1"},
{"username": "test3", "password": "333333", "user_agent": "IOS/10.2"}},
},
{
map[string]interface{}{},
nil,
},
{
nil,
nil,
},
}
for _, data := range testData {
value, _ := parseParameters(data.rawVars, map[string]interface{}{})
if !assert.Equal(t, data.expectVars, value) {
t.Fail()
}
}
}
func TestParseParametersError(t *testing.T) {
testData := []struct {
rawVars map[string]interface{}
}{
{
map[string]interface{}{"username_password": "${parameterize(examples/account.csv)}", "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
{
map[string]interface{}{"username-password": "${parameterize(examples/account.csv)}", "user-agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
{
map[string]interface{}{"username-password": "${param(examples/account.csv)}", "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
}
for _, data := range testData {
_, err := parseParameters(data.rawVars, map[string]interface{}{})
if !assert.Error(t, err) {
t.Fail()
}
}
}
+3
View File
@@ -131,6 +131,9 @@ func (r *hrpRunner) runCase(testcase *TestCase) error {
cfg := config.ToStruct() cfg := config.ToStruct()
log.Info().Str("testcase", config.Name()).Msg("run testcase start") log.Info().Str("testcase", config.Name()).Msg("run testcase start")
parameters := getParameters(config) parameters := getParameters(config)
if parameters == nil {
parameters = []map[string]interface{}{{}}
}
for _, parameter := range parameters { for _, parameter := range parameters {
cfg.Variables = mergeVariables(parameter, cfg.Variables) cfg.Variables = mergeVariables(parameter, cfg.Variables)
r.startTime = time.Now() r.startTime = time.Now()
-41
View File
@@ -1,41 +0,0 @@
package hrp
import (
"math/rand"
"strings"
"time"
)
func contains(s []string, e string) bool {
for _, a := range s {
if strings.EqualFold(a, e) {
return true
}
}
return false
}
func shuffleCartesianProduct(slice []map[string]interface{}) {
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{} {
var cartesianProduct []map[string]interface{}
cartesianProduct = params[0]
for i := 0; i < len(params)-1; i++ {
var tempProduct []map[string]interface{}
for _, param1 := range cartesianProduct {
for _, param2 := range params[i+1] {
tempProduct = append(tempProduct, mergeVariables(param1, param2))
}
}
cartesianProduct = tempProduct
}
return cartesianProduct
}